diff --git a/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj b/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj index 604cbd2592..f4a39df579 100644 --- a/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj +++ b/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj @@ -49,13 +49,11 @@ ..\packages\SqlServerCE.4.0.0.1\lib\System.Data.SqlServerCe.dll - False - False + True ..\packages\SqlServerCE.4.0.0.1\lib\System.Data.SqlServerCe.Entity.dll - False - False + True diff --git a/src/SQLCE4Umbraco/SqlCeApplicationBlock.cs b/src/SQLCE4Umbraco/SqlCeApplicationBlock.cs index fd778bbfb3..2dd0f26e90 100644 --- a/src/SQLCE4Umbraco/SqlCeApplicationBlock.cs +++ b/src/SQLCE4Umbraco/SqlCeApplicationBlock.cs @@ -240,7 +240,7 @@ namespace SqlCE4Umbraco { var cmd = trx == null ? new SqlCeCommand(commandText, conn) : new SqlCeCommand(commandText, conn, trx); AttachParameters(cmd, commandParameters); - return cmd.ExecuteReader(CommandBehavior.CloseConnection); + return cmd.ExecuteReader(); } catch { diff --git a/src/SQLCE4Umbraco/app.config b/src/SQLCE4Umbraco/app.config index cb532bd57a..1ac57258d3 100644 --- a/src/SQLCE4Umbraco/app.config +++ b/src/SQLCE4Umbraco/app.config @@ -4,7 +4,7 @@ - + @@ -32,4 +32,7 @@ - + + + + diff --git a/src/Umbraco.Compat7/Core/ApplicationContext.cs b/src/Umbraco.Compat7/Core/ApplicationContext.cs index 40c295c190..cf423ce1a5 100644 --- a/src/Umbraco.Compat7/Core/ApplicationContext.cs +++ b/src/Umbraco.Compat7/Core/ApplicationContext.cs @@ -1,6 +1,7 @@ using System; using Umbraco.Core.Cache; using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; using Umbraco.Core.Services; // ReSharper disable once CheckNamespace @@ -10,7 +11,7 @@ namespace Umbraco.Core { private ApplicationContext() { - DatabaseContext = new DatabaseContext(DI.Current.DatabaseFactory); + DatabaseContext = new DatabaseContext(DI.Current.Container.GetInstance()); } public static ApplicationContext Current { get; } = new ApplicationContext(); diff --git a/src/Umbraco.Compat7/Core/DatabaseContext.cs b/src/Umbraco.Compat7/Core/DatabaseContext.cs index 9048006675..9c02cbac1d 100644 --- a/src/Umbraco.Compat7/Core/DatabaseContext.cs +++ b/src/Umbraco.Compat7/Core/DatabaseContext.cs @@ -21,9 +21,7 @@ namespace Umbraco.Core /// used. public DatabaseContext(IUmbracoDatabaseFactory databaseFactory) { - if (databaseFactory == null) throw new ArgumentNullException(nameof(databaseFactory)); - - _databaseFactory = databaseFactory; + _databaseFactory = databaseFactory ?? throw new ArgumentNullException(nameof(databaseFactory)); } /// @@ -50,16 +48,7 @@ namespace Umbraco.Core /// Gets an ambient database for doing CRUD operations against custom tables that resides in the Umbraco database. /// /// Should not be used for operation against standard Umbraco tables; as services should be used instead. - public IUmbracoDatabase Database => _databaseFactory.GetDatabase(); - - /// - /// Gets an ambient database scope. - /// - /// A disposable object representing the scope. - public IDisposable CreateDatabaseScope() // fixme - move over to factory - { - return _databaseFactory.CreateScope(); - } + public IUmbracoDatabase Database => throw new NotImplementedException(); // there's no magic? /// /// Gets a value indicating whether the database is configured. diff --git a/src/Umbraco.Core/BindingRedirects.cs b/src/Umbraco.Core/BindingRedirects.cs new file mode 100644 index 0000000000..4fadb4db70 --- /dev/null +++ b/src/Umbraco.Core/BindingRedirects.cs @@ -0,0 +1,49 @@ +using System; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Web; +using Umbraco.Core; + +[assembly: PreApplicationStartMethod(typeof(BindingRedirects), "Initialize")] + +namespace Umbraco.Core +{ + /// + /// Manages any assembly binding redirects that cannot be done via config (i.e. unsigned --> signed assemblies) + /// + public sealed class BindingRedirects + { + public static void Initialize() + { + // this only gets called when an assembly can't be resolved + AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; + } + + private static readonly Regex Log4NetAssemblyPattern = new Regex("log4net, Version=([\\d\\.]+?), Culture=neutral, PublicKeyToken=\\w+$", RegexOptions.Compiled); + private const string Log4NetReplacement = "log4net, Version=2.0.8.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a"; + + /// + /// This is used to do an assembly binding redirect via code - normally required due to signature changes in assemblies + /// + /// + /// + /// + private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) + { + //log4net: + if (Log4NetAssemblyPattern.IsMatch(args.Name) && args.Name != Log4NetReplacement) + { + return Assembly.Load(Log4NetAssemblyPattern.Replace(args.Name, Log4NetReplacement)); + } + + //AutoMapper: + // ensure the assembly is indeed AutoMapper and that the PublicKeyToken is null before trying to Load again + // do NOT just replace this with 'return Assembly', as it will cause an infinite loop -> stackoverflow + if (args.Name.StartsWith("AutoMapper") && args.Name.EndsWith("PublicKeyToken=null")) + return Assembly.Load(args.Name.Replace(", PublicKeyToken=null", ", PublicKeyToken=be96cd2c38ef1005")); + + return null; + + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/CacheHelper.cs b/src/Umbraco.Core/Cache/CacheHelper.cs index cbf2836232..157cedb3eb 100644 --- a/src/Umbraco.Core/Cache/CacheHelper.cs +++ b/src/Umbraco.Core/Cache/CacheHelper.cs @@ -14,8 +14,11 @@ namespace Umbraco.Core.Cache private static readonly ICacheProvider NullRequestCache = new NullCacheProvider(); private static readonly ICacheProvider NullStaticCache = new NullCacheProvider(); private static readonly IRuntimeCacheProvider NullRuntimeCache = new NullCacheProvider(); - - /// + private static readonly IsolatedRuntimeCache NullIsolatedCache = new IsolatedRuntimeCache(_ => NullRuntimeCache); + + public static CacheHelper NoCache { get; } = new CacheHelper(NullRuntimeCache, NullStaticCache, NullRequestCache, NullIsolatedCache); + + /// /// Creates a cache helper with disabled caches /// /// @@ -24,7 +27,10 @@ namespace Umbraco.Core.Cache /// public static CacheHelper CreateDisabledCacheHelper() { - return new CacheHelper(NullRuntimeCache, NullStaticCache, NullRequestCache, new IsolatedRuntimeCache(t => NullRuntimeCache)); + // do *not* return NoCache + // NoCache is a special instance that is detected by RepositoryBase and disables all cache policies + // CreateDisabledCacheHelper is used in tests to use no cache, *but* keep all cache policies + return new CacheHelper(NullRuntimeCache, NullStaticCache, NullRequestCache, NullIsolatedCache); } /// diff --git a/src/Umbraco.Core/Cache/CacheKeys.cs b/src/Umbraco.Core/Cache/CacheKeys.cs index 92f8529bbe..d0155ae520 100644 --- a/src/Umbraco.Core/Cache/CacheKeys.cs +++ b/src/Umbraco.Core/Cache/CacheKeys.cs @@ -60,8 +60,8 @@ namespace Umbraco.Core.Cache [UmbracoWillObsolete("This cache key is only used for legacy business logic caching, remove in v8")] public const string ContentTypePropertiesCacheKey = "ContentType_PropertyTypes_Content:"; - - [UmbracoWillObsolete("This cache key is only used for legacy business logic caching, remove in v8")] + + [Obsolete("No longer used and will be removed in v8")] public const string PropertyTypeCacheKey = "UmbracoPropertyTypeCache"; [Obsolete("This is no longer used and will be removed from the codebase in the future")] diff --git a/src/Umbraco.Core/Cache/DeepCloneRuntimeCacheProvider.cs b/src/Umbraco.Core/Cache/DeepCloneRuntimeCacheProvider.cs index 0ae721943d..456c2955f0 100644 --- a/src/Umbraco.Core/Cache/DeepCloneRuntimeCacheProvider.cs +++ b/src/Umbraco.Core/Cache/DeepCloneRuntimeCacheProvider.cs @@ -22,14 +22,20 @@ namespace Umbraco.Core.Cache /// internal class DeepCloneRuntimeCacheProvider : IRuntimeCacheProvider, IRuntimeCacheProviderWrapper { - public IRuntimeCacheProvider InnerProvider { get; private set; } + public IRuntimeCacheProvider InnerProvider { get; } public DeepCloneRuntimeCacheProvider(IRuntimeCacheProvider innerProvider) { + var type = typeof (DeepCloneRuntimeCacheProvider); + + if (innerProvider.GetType() == type) + throw new InvalidOperationException($"A {type} cannot wrap another instance of {type}."); + InnerProvider = innerProvider; } #region Clear - doesn't require any changes + public void ClearAllCache() { InnerProvider.ClearAllCache(); @@ -63,7 +69,8 @@ namespace Umbraco.Core.Cache public void ClearCacheByKeyExpression(string regexString) { InnerProvider.ClearCacheByKeyExpression(regexString); - } + } + #endregion public IEnumerable GetCacheItemsByKeySearch(string keyStartsWith) @@ -105,9 +112,11 @@ namespace Umbraco.Core.Cache var value = result.Value; // force evaluation now - this may throw if cacheItem throws, and then nothing goes into cache if (value == null) return null; // do not store null values (backward compat) + // clone / reset to go into the cache return CheckCloneableAndTracksChanges(value); }, timeout, isSliding, priority, removedCallback, dependentFiles); + // clone / reset to go into the cache return CheckCloneableAndTracksChanges(cached); } @@ -119,6 +128,7 @@ namespace Umbraco.Core.Cache var value = result.Value; // force evaluation now - this may throw if cacheItem throws, and then nothing goes into cache if (value == null) return null; // do not store null values (backward compat) + // clone / reset to go into the cache return CheckCloneableAndTracksChanges(value); }, timeout, isSliding, priority, removedCallback, dependentFiles); } diff --git a/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs index 0d601fbea6..67df32d3ea 100644 --- a/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Scoping; namespace Umbraco.Core.Cache { @@ -19,14 +20,18 @@ namespace Umbraco.Core.Cache internal class DefaultRepositoryCachePolicy : RepositoryCachePolicyBase where TEntity : class, IAggregateRoot { - private static readonly TEntity[] EmptyEntities = new TEntity[0]; + private static readonly TEntity[] EmptyEntities = new TEntity[0]; // const private readonly RepositoryCachePolicyOptions _options; public DefaultRepositoryCachePolicy(IRuntimeCacheProvider cache, RepositoryCachePolicyOptions options) : base(cache) { - if (options == null) throw new ArgumentNullException(nameof(options)); - _options = options; + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + + public override IRepositoryCachePolicy Scoped(IRuntimeCacheProvider runtimeCache, IScope scope) + { + return new ScopedRepositoryCachePolicy(this, runtimeCache, scope); } protected string GetEntityCacheKey(object id) diff --git a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs index 26d81830c0..d362f0482b 100644 --- a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core.Collections; using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Scoping; namespace Umbraco.Core.Cache { @@ -23,16 +24,19 @@ namespace Umbraco.Core.Cache private readonly Func _entityGetId; private readonly bool _expires; - public FullDataSetRepositoryCachePolicy(IRuntimeCacheProvider cache, - Func entityGetId, - bool expires) + public FullDataSetRepositoryCachePolicy(IRuntimeCacheProvider cache, Func entityGetId, bool expires) : base(cache) { _entityGetId = entityGetId; _expires = expires; } - protected static readonly TId[] EmptyIds = new TId[0]; + public override IRepositoryCachePolicy Scoped(IRuntimeCacheProvider runtimeCache, IScope scope) + { + return new ScopedRepositoryCachePolicy(this, runtimeCache, scope); + } + + protected static readonly TId[] EmptyIds = new TId[0]; // const protected string GetEntityTypeCacheKey() { diff --git a/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs index 11a3cf6bb9..eeb4f77de3 100644 --- a/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Scoping; namespace Umbraco.Core.Cache { @@ -17,6 +18,16 @@ namespace Umbraco.Core.Cache // it is not *that* complicated but then RepositoryBase needs to have a TRepository generic // type parameter and it all becomes convoluted - keeping it simple for the time being. + /// + /// Creates a scoped version of this cache policy. + /// + /// The global isolated runtime cache for this policy. + /// The scope. + /// When a policy is scoped, it means that it has been created with a scoped + /// isolated runtime cache, and now it needs to be wrapped into something that can apply + /// changes to the global isolated runtime cache. + IRepositoryCachePolicy Scoped(IRuntimeCacheProvider runtimeCache, IScope scope); + /// /// Gets an entity from the cache, else from the repository. /// diff --git a/src/Umbraco.Core/Cache/NoCacheRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/NoCacheRepositoryCachePolicy.cs index cfcee5d728..ad0f6998cc 100644 --- a/src/Umbraco.Core/Cache/NoCacheRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/NoCacheRepositoryCachePolicy.cs @@ -2,30 +2,21 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Scoping; namespace Umbraco.Core.Cache { - class NoCacheRepositoryCachePolicy : IRepositoryCachePolicy + internal class NoCacheRepositoryCachePolicy : IRepositoryCachePolicy where TEntity : class, IAggregateRoot { - public void ClearAll() - { - // nothing to clear - not caching - } + private NoCacheRepositoryCachePolicy() + { } - public void Create(TEntity entity, Action persistNew) - { - persistNew(entity); - } + public static NoCacheRepositoryCachePolicy Instance { get; } = new NoCacheRepositoryCachePolicy(); - public void Delete(TEntity entity, Action persistDeleted) + public IRepositoryCachePolicy Scoped(IRuntimeCacheProvider runtimeCache, IScope scope) { - persistDeleted(entity); - } - - public bool Exists(TId id, Func performExists, Func> performGetAll) - { - return performExists(id); + throw new NotImplementedException(); } public TEntity Get(TId id, Func performGet, Func> performGetAll) @@ -33,19 +24,37 @@ namespace Umbraco.Core.Cache return performGet(id); } - public TEntity[] GetAll(TId[] ids, Func> performGetAll) - { - return performGetAll(ids).ToArray(); - } - public TEntity GetCached(TId id) { return null; } + public bool Exists(TId id, Func performExists, Func> performGetAll) + { + return performExists(id); + } + + public void Create(TEntity entity, Action persistNew) + { + persistNew(entity); + } + public void Update(TEntity entity, Action persistUpdated) { persistUpdated(entity); } + + public void Delete(TEntity entity, Action persistDeleted) + { + persistDeleted(entity); + } + + public TEntity[] GetAll(TId[] ids, Func> performGetAll) + { + return performGetAll(ids).ToArray(); + } + + public void ClearAll() + { } } } diff --git a/src/Umbraco.Core/Cache/RepositoryCachePolicyBase.cs b/src/Umbraco.Core/Cache/RepositoryCachePolicyBase.cs index a5bfa5fcb3..2897a6c1d1 100644 --- a/src/Umbraco.Core/Cache/RepositoryCachePolicyBase.cs +++ b/src/Umbraco.Core/Cache/RepositoryCachePolicyBase.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Scoping; namespace Umbraco.Core.Cache { @@ -14,9 +15,10 @@ namespace Umbraco.Core.Cache { protected RepositoryCachePolicyBase(IRuntimeCacheProvider cache) { - if (cache == null) throw new ArgumentNullException(nameof(cache)); - Cache = cache; - } + Cache = cache ?? throw new ArgumentNullException(nameof(cache)); + } + + public abstract IRepositoryCachePolicy Scoped(IRuntimeCacheProvider runtimeCache, IScope scope); protected IRuntimeCacheProvider Cache { get; } diff --git a/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs b/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs index 14cef76db6..607c4c210c 100644 --- a/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs +++ b/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs @@ -30,7 +30,7 @@ namespace Umbraco.Core.Cache /// /// Callback required to get count for GetAllCacheValidateCount /// - public Func PerformCount { get; private set; } + public Func PerformCount { get; set; } /// /// True/false as to validate the total item count when all items are returned from cache, the default is true but this @@ -41,7 +41,7 @@ namespace Umbraco.Core.Cache /// setting this to return false will improve performance of GetAll cache with no params but should only be used /// for specific circumstances /// - public bool GetAllCacheValidateCount { get; private set; } + public bool GetAllCacheValidateCount { get; set; } /// /// True if the GetAll method will cache that there are zero results so that the db is not hit when there are no results found diff --git a/src/Umbraco.Core/Cache/ScopedRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/ScopedRepositoryCachePolicy.cs new file mode 100644 index 0000000000..f4244a49a1 --- /dev/null +++ b/src/Umbraco.Core/Cache/ScopedRepositoryCachePolicy.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Scoping; + +namespace Umbraco.Core.Cache +{ + internal class ScopedRepositoryCachePolicy : IRepositoryCachePolicy + where TEntity : class, IAggregateRoot + { + private readonly IRepositoryCachePolicy _cachePolicy; + private readonly IRuntimeCacheProvider _globalIsolatedCache; + private readonly IScope _scope; + + public ScopedRepositoryCachePolicy(IRepositoryCachePolicy cachePolicy, IRuntimeCacheProvider globalIsolatedCache, IScope scope) + { + _cachePolicy = cachePolicy; + _globalIsolatedCache = globalIsolatedCache; + _scope = scope; + } + + public IRepositoryCachePolicy Scoped(IRuntimeCacheProvider runtimeCache, IScope scope) + { + throw new InvalidOperationException(); // obviously + } + + public TEntity Get(TId id, Func performGet, Func> performGetAll) + { + // loads into the local cache only, ok for now + return _cachePolicy.Get(id, performGet, performGetAll); + } + + public TEntity GetCached(TId id) + { + // loads into the local cache only, ok for now + return _cachePolicy.GetCached(id); + } + + public bool Exists(TId id, Func performExists, Func> performGetAll) + { + // loads into the local cache only, ok for now + return _cachePolicy.Exists(id, performExists, performGetAll); + } + + public void Create(TEntity entity, Action persistNew) + { + // writes into the local cache + _cachePolicy.Create(entity, persistNew); + } + + public void Update(TEntity entity, Action persistUpdated) + { + // writes into the local cache + _cachePolicy.Update(entity, persistUpdated); + } + + public void Delete(TEntity entity, Action persistDeleted) + { + // deletes the local cache + _cachePolicy.Delete(entity, persistDeleted); + } + + public TEntity[] GetAll(TId[] ids, Func> performGetAll) + { + // loads into the local cache only, ok for now + return _cachePolicy.GetAll(ids, performGetAll); + } + + public void ClearAll() + { + // clears the local cache + _cachePolicy.ClearAll(); + } + } +} diff --git a/src/Umbraco.Core/Cache/SingleItemsOnlyRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/SingleItemsOnlyRepositoryCachePolicy.cs index f63ce2c023..7ba7d445fe 100644 --- a/src/Umbraco.Core/Cache/SingleItemsOnlyRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/SingleItemsOnlyRepositoryCachePolicy.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Cache @@ -17,7 +15,7 @@ namespace Umbraco.Core.Cache internal class SingleItemsOnlyRepositoryCachePolicy : DefaultRepositoryCachePolicy where TEntity : class, IAggregateRoot { - public SingleItemsOnlyRepositoryCachePolicy(IRuntimeCacheProvider cache, RepositoryCachePolicyOptions options) + public SingleItemsOnlyRepositoryCachePolicy(IRuntimeCacheProvider cache, RepositoryCachePolicyOptions options) : base(cache, options) { } diff --git a/src/Umbraco.Core/CodeAnnotations/UmbracoUdiTypeAttribute.cs b/src/Umbraco.Core/CodeAnnotations/UmbracoUdiTypeAttribute.cs new file mode 100644 index 0000000000..0bb9de6c86 --- /dev/null +++ b/src/Umbraco.Core/CodeAnnotations/UmbracoUdiTypeAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace Umbraco.Core.CodeAnnotations +{ + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] + internal class UmbracoUdiTypeAttribute : Attribute + { + public string UdiType { get; private set; } + + public UmbracoUdiTypeAttribute(string udiType) + { + UdiType = udiType; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Components/BootLoader.cs b/src/Umbraco.Core/Components/BootLoader.cs index bca9990ddb..efd29a24da 100644 --- a/src/Umbraco.Core/Components/BootLoader.cs +++ b/src/Umbraco.Core/Components/BootLoader.cs @@ -7,6 +7,7 @@ using LightInject; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; +using Umbraco.Core.Scoping; namespace Umbraco.Core.Components { @@ -49,9 +50,10 @@ namespace Umbraco.Core.Components InstanciateComponents(orderedComponentTypes); ComposeComponents(level); - using (_container.GetInstance().CreateScope()) + using (var scope = _container.GetInstance().CreateScope()) { InitializeComponents(); + scope.Complete(); } // rejoice! diff --git a/src/Umbraco.Core/Configuration/ContentXmlStorage.cs b/src/Umbraco.Core/Configuration/ContentXmlStorage.cs new file mode 100644 index 0000000000..7cbbc70675 --- /dev/null +++ b/src/Umbraco.Core/Configuration/ContentXmlStorage.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Core.Configuration +{ + internal enum ContentXmlStorage + { + Default, + AspNetTemp, + EnvironmentTemp + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/CoreDebug.cs b/src/Umbraco.Core/Configuration/CoreDebug.cs new file mode 100644 index 0000000000..71d0f24941 --- /dev/null +++ b/src/Umbraco.Core/Configuration/CoreDebug.cs @@ -0,0 +1,32 @@ +using System; + +namespace Umbraco.Core.Configuration +{ + internal static class CoreDebugExtensions + { + private static CoreDebug _coreDebug; + + public static CoreDebug CoreDebug(this UmbracoConfig config) + { + return _coreDebug ?? (_coreDebug = new CoreDebug()); + } + } + + internal class CoreDebug + { + public CoreDebug() + { + var appSettings = System.Configuration.ConfigurationManager.AppSettings; + LogUncompletedScopes = string.Equals("true", appSettings["Umbraco.CoreDebug.LogUncompletedScopes"], StringComparison.OrdinalIgnoreCase); + DumpOnTimeoutThreadAbort = string.Equals("true", appSettings["Umbraco.CoreDebug.DumpOnTimeoutThreadAbort"], StringComparison.OrdinalIgnoreCase); + } + + // when true, Scope logs the stack trace for any scope that gets disposed without being completed. + // this helps troubleshooting rogue scopes that we forget to complete + public bool LogUncompletedScopes { get; } + + // when true, the Logger creates a minidump of w3wp in ~/App_Data/MiniDump whenever it logs + // an error due to a ThreadAbortException that is due to a timeout. + public bool DumpOnTimeoutThreadAbort { get; } + } +} diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs index 16bb57fd07..ede64c2d24 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs @@ -205,10 +205,6 @@ namespace Umbraco.Core.Configuration } } - //TODO: Move these to constants! - public const string UmbracoConnectionName = "umbracoDbDSN"; - public const string UmbracoMigrationName = "Umbraco"; - /// /// Gets or sets the configuration status. This will return the version number of the currently installed umbraco instance. /// @@ -453,12 +449,25 @@ namespace Umbraco.Core.Configuration } internal static bool ContentCacheXmlStoredInCodeGen + { + get { return ContentCacheXmlStorageLocation == ContentXmlStorage.AspNetTemp; } + } + + internal static ContentXmlStorage ContentCacheXmlStorageLocation { get { - //defaults to false - return ConfigurationManager.AppSettings.ContainsKey("umbracoContentXMLUseLocalTemp") - && bool.Parse(ConfigurationManager.AppSettings["umbracoContentXMLUseLocalTemp"]); //default to false + if (ConfigurationManager.AppSettings.ContainsKey("umbracoContentXMLStorage")) + { + return Enum.Parse(ConfigurationManager.AppSettings["umbracoContentXMLStorage"]); + } + if (ConfigurationManager.AppSettings.ContainsKey("umbracoContentXMLUseLocalTemp")) + { + return bool.Parse(ConfigurationManager.AppSettings["umbracoContentXMLUseLocalTemp"]) + ? ContentXmlStorage.AspNetTemp + : ContentXmlStorage.Default; + } + return ContentXmlStorage.Default; } } diff --git a/src/Umbraco.Core/Configuration/InfrastructureSettings/Infrastructure.cs b/src/Umbraco.Core/Configuration/InfrastructureSettings/Infrastructure.cs deleted file mode 100644 index 8a8f4454bc..0000000000 --- a/src/Umbraco.Core/Configuration/InfrastructureSettings/Infrastructure.cs +++ /dev/null @@ -1,190 +0,0 @@ -using System.Configuration; - -namespace Umbraco.Core.Configuration.InfrastructureSettings -{ - public class Infrastructure : ConfigurationSection - { - private const string InfrastructureSectionName = "umbraco/infrastructure"; - - public static Infrastructure Instance - { - get { return (Infrastructure) ConfigurationManager.GetSection(InfrastructureSectionName); } - } - - #region RepositoriesSection Property - - internal const string RepositoriesPropertyName = "repositories"; - - [ConfigurationProperty(RepositoriesPropertyName, IsRequired = true, IsKey = false, IsDefaultCollection = false)] - public Repositories Repositories - { - get { return ((Repositories)base[RepositoriesPropertyName]); } - set { base[RepositoriesPropertyName] = value; } - } - - #endregion - - #region PublishingStrategy Property - - internal const string PublishingStrategyPropertyName = "publishingStrategy"; - - [ConfigurationProperty(PublishingStrategyPropertyName, IsRequired = true, IsKey = false, IsDefaultCollection = false)] - public PublishingProvider PublishingStrategy - { - get { return ((PublishingProvider)base[PublishingStrategyPropertyName]); } - set { base[PublishingStrategyPropertyName] = value; } - } - - #endregion - } - - public class Repositories : ConfigurationElement - { - [ConfigurationProperty("", IsDefaultCollection = true, IsRequired = true)] - public RepositoryElementCollection Repository - { - get { return ((RepositoryElementCollection)(base[""])); } - } - } - - [ConfigurationCollection(typeof(Repository), CollectionType = ConfigurationElementCollectionType.BasicMapAlternate, AddItemName = RepositoryPropertyName)] - public class RepositoryElementCollection : ConfigurationElementCollection - { - internal const string RepositoryPropertyName = "repository"; - - public override ConfigurationElementCollectionType CollectionType - { - get - { - return ConfigurationElementCollectionType.BasicMapAlternate; - } - } - - protected override string ElementName - { - get - { - return RepositoryPropertyName; - } - } - - protected override bool IsElementName(string elementName) - { - return elementName == RepositoryPropertyName; - } - - protected override object GetElementKey(ConfigurationElement element) - { - return ((Repository)element).InterfaceShortTypeName; - } - - protected override ConfigurationElement CreateNewElement() - { - return new Repository(); - } - - #region Indexer - - public Repository this[int index] - { - get { return (Repository)base.BaseGet(index); } - } - - public Repository this[string interfaceShortTypeName] - { - get { return (Repository)base.BaseGet(interfaceShortTypeName); } - } - - #endregion - - #region Add - - public void Add(Repository repository) - { - BaseAdd(repository); - } - - #endregion - - #region Remove - - public void Remove(Repository repository) - { - BaseRemove(repository); - } - - #endregion - - #region GetItem - - public Repository GetItemAt(int index) - { - return (Repository)BaseGet(index); - } - - public Repository GetItemByKey(string interfaceShortTypeName) - { - return (Repository)BaseGet(interfaceShortTypeName); - } - - #endregion - - public bool ContainsKey(string interfaceShortName) - { - bool result = false; - object[] keys = this.BaseGetAllKeys(); - foreach (object key in keys) - { - if ((string)key == interfaceShortName) - { - result = true; - break; - - } - } - return result; - } - } - - public class Repository : ConfigurationElement - { - internal const string InterfaceShortTypeNamePropertyName = "interfaceShortTypeName"; - - [ConfigurationPropertyAttribute(InterfaceShortTypeNamePropertyName, IsRequired = true, IsKey = true, IsDefaultCollection = false)] - public string InterfaceShortTypeName - { - get { return (string) base[InterfaceShortTypeNamePropertyName]; } - set { base[InterfaceShortTypeNamePropertyName] = value; } - } - - internal const string RepositoryFullTypeNamePropertyName = "repositoryFullTypeName"; - - [ConfigurationPropertyAttribute(RepositoryFullTypeNamePropertyName, IsRequired = true, IsKey = false, IsDefaultCollection = false)] - public string RepositoryFullTypeName - { - get { return (string)base[RepositoryFullTypeNamePropertyName]; } - set { base[RepositoryFullTypeNamePropertyName] = value; } - } - - internal const string CacheProviderFullTypeNamePropertyName = "cacheProviderFullTypeName"; - - [ConfigurationPropertyAttribute(CacheProviderFullTypeNamePropertyName, IsRequired = true, IsKey = false, IsDefaultCollection = false)] - public string CacheProviderFullTypeName - { - get { return (string)base[CacheProviderFullTypeNamePropertyName]; } - set { base[CacheProviderFullTypeNamePropertyName] = value; } - } - } - - public class PublishingProvider : ConfigurationElement - { - internal const string TypePropertyName = "type"; - - [ConfigurationPropertyAttribute(TypePropertyName, IsRequired = true, IsKey = false, IsDefaultCollection = false)] - public string Type - { - get { return (string)base[TypePropertyName]; } - set { base[TypePropertyName] = value; } - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs index 12df34d05d..9f1c61ec09 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs @@ -5,8 +5,7 @@ using Umbraco.Core.Macros; namespace Umbraco.Core.Configuration.UmbracoSettings { - - internal class ContentElement : ConfigurationElement, IContentSection + internal class ContentElement : UmbracoConfigurationElement, IContentSection { [ConfigurationProperty("imaging")] internal ContentImagingElement Imaging @@ -23,25 +22,13 @@ namespace Umbraco.Core.Configuration.UmbracoSettings [ConfigurationProperty("ResolveUrlsFromTextString")] internal InnerTextConfigurationElement ResolveUrlsFromTextString { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["ResolveUrlsFromTextString"], - //set the default - false); - } + get { return GetOptionalTextElement("ResolveUrlsFromTextString", false); } } [ConfigurationProperty("UploadAllowDirectories")] internal InnerTextConfigurationElement UploadAllowDirectories { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["UploadAllowDirectories"], - //set the default - true); - } + get { return GetOptionalTextElement("UploadAllowDirectories", true); } } public IEnumerable Error404Collection @@ -64,86 +51,44 @@ namespace Umbraco.Core.Configuration.UmbracoSettings [ConfigurationProperty("ensureUniqueNaming")] internal InnerTextConfigurationElement EnsureUniqueNaming { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["ensureUniqueNaming"], - //set the default - true); - } + get { return GetOptionalTextElement("ensureUniqueNaming", true); } } [ConfigurationProperty("XmlCacheEnabled")] internal InnerTextConfigurationElement XmlCacheEnabled { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["XmlCacheEnabled"], - //set the default - true); - } + get { return GetOptionalTextElement("XmlCacheEnabled", true); } } [ConfigurationProperty("ContinouslyUpdateXmlDiskCache")] internal InnerTextConfigurationElement ContinouslyUpdateXmlDiskCache { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["ContinouslyUpdateXmlDiskCache"], - //set the default - true); - } + get { return GetOptionalTextElement("ContinouslyUpdateXmlDiskCache", true); } } [ConfigurationProperty("XmlContentCheckForDiskChanges")] internal InnerTextConfigurationElement XmlContentCheckForDiskChanges { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["XmlContentCheckForDiskChanges"], - //set the default - false); - } + get { return GetOptionalTextElement("XmlContentCheckForDiskChanges", false); } } [ConfigurationProperty("EnableSplashWhileLoading")] internal InnerTextConfigurationElement EnableSplashWhileLoading { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["EnableSplashWhileLoading"], - //set the default - false); - } + get { return GetOptionalTextElement("EnableSplashWhileLoading", false); } } [ConfigurationProperty("PropertyContextHelpOption")] internal InnerTextConfigurationElement PropertyContextHelpOption { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["PropertyContextHelpOption"], - //set the default - "text"); - } + get { return GetOptionalTextElement("PropertyContextHelpOption", "text"); } } [ConfigurationProperty("ForceSafeAliases")] internal InnerTextConfigurationElement ForceSafeAliases { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["ForceSafeAliases"], - //set the default - true); - } + get { return GetOptionalTextElement("ForceSafeAliases", true); } } [ConfigurationProperty("PreviewBadge")] @@ -151,110 +96,68 @@ namespace Umbraco.Core.Configuration.UmbracoSettings { get { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["PreviewBadge"], - //set the default - @"In Preview Mode - click to end"); + return GetOptionalTextElement("PreviewBadge", @"In Preview Mode - click to end"); } } [ConfigurationProperty("UmbracoLibraryCacheDuration")] internal InnerTextConfigurationElement UmbracoLibraryCacheDuration { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["UmbracoLibraryCacheDuration"], - //set the default - 1800); - - } + get { return GetOptionalTextElement("UmbracoLibraryCacheDuration", 1800); } } [ConfigurationProperty("MacroErrors")] internal InnerTextConfigurationElement MacroErrors { - get - { - - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["MacroErrors"], - //set the default - MacroErrorBehaviour.Inline); - } + get { return GetOptionalTextElement("MacroErrors", MacroErrorBehaviour.Inline); } } [ConfigurationProperty("disallowedUploadFiles")] internal CommaDelimitedConfigurationElement DisallowedUploadFiles { - get - { - return new OptionalCommaDelimitedConfigurationElement( - (CommaDelimitedConfigurationElement)this["disallowedUploadFiles"], - //set the default - new[] { "ashx", "aspx", "ascx", "config", "cshtml", "vbhtml", "asmx", "air", "axd" }); - - } + get { return GetOptionalDelimitedElement("disallowedUploadFiles", new[] {"ashx", "aspx", "ascx", "config", "cshtml", "vbhtml", "asmx", "air", "axd"}); } } [ConfigurationProperty("cloneXmlContent")] internal InnerTextConfigurationElement CloneXmlContent { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["cloneXmlContent"], - //set the default - true); - } + get { return GetOptionalTextElement("cloneXmlContent", true); } } [ConfigurationProperty("GlobalPreviewStorageEnabled")] internal InnerTextConfigurationElement GlobalPreviewStorageEnabled { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["GlobalPreviewStorageEnabled"], - //set the default - false); - } + get { return GetOptionalTextElement("GlobalPreviewStorageEnabled", false); } } [ConfigurationProperty("defaultDocumentTypeProperty")] internal InnerTextConfigurationElement DefaultDocumentTypeProperty { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["defaultDocumentTypeProperty"], - //set the default - "Textstring"); - } + get { return GetOptionalTextElement("defaultDocumentTypeProperty", "Textstring"); } + } + + [ConfigurationProperty("showDeprecatedPropertyEditors")] + internal InnerTextConfigurationElement ShowDeprecatedPropertyEditors + { + get { return GetOptionalTextElement("showDeprecatedPropertyEditors", false); } } [ConfigurationProperty("EnableInheritedDocumentTypes")] internal InnerTextConfigurationElement EnableInheritedDocumentTypes { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement) this["EnableInheritedDocumentTypes"], - //set the default - true); - } + get { return GetOptionalTextElement("EnableInheritedDocumentTypes", true); } } [ConfigurationProperty("EnableInheritedMediaTypes")] internal InnerTextConfigurationElement EnableInheritedMediaTypes { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["EnableInheritedMediaTypes"], - //set the default - true); - } + get { return GetOptionalTextElement("EnableInheritedMediaTypes", true); } + } + + [ConfigurationProperty("loginBackgroundImage")] + internal InnerTextConfigurationElement LoginBackgroundImage + { + get { return GetOptionalTextElement("loginBackgroundImage", string.Empty); } } string IContentSection.NotificationEmailAddress @@ -386,5 +289,10 @@ namespace Umbraco.Core.Configuration.UmbracoSettings { get { return EnableInheritedMediaTypes; } } + + string IContentSection.LoginBackgroundImage + { + get { return LoginBackgroundImage; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentScriptEditorElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentScriptEditorElement.cs index 0d14609caa..f60e964a04 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentScriptEditorElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentScriptEditorElement.cs @@ -3,42 +3,24 @@ using System.Configuration; namespace Umbraco.Core.Configuration.UmbracoSettings { - internal class ContentScriptEditorElement : ConfigurationElement + internal class ContentScriptEditorElement : UmbracoConfigurationElement { [ConfigurationProperty("scriptFolderPath")] internal InnerTextConfigurationElement ScriptFolderPath { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["scriptFolderPath"], - //set the default - "/scripts"); - } + get { return GetOptionalTextElement("scriptFolderPath", "/scripts"); } } [ConfigurationProperty("scriptFileTypes")] internal OptionalCommaDelimitedConfigurationElement ScriptFileTypes { - get - { - return new OptionalCommaDelimitedConfigurationElement( - (OptionalCommaDelimitedConfigurationElement)this["scriptFileTypes"], - //set the default - new[] { "js", "xml" }); - } + get { return GetOptionalDelimitedElement("scriptFileTypes", new[] {"js", "xml"}); } } [ConfigurationProperty("scriptDisableEditor")] internal InnerTextConfigurationElement ScriptEditorDisable { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement) this["scriptDisableEditor"], - //set the default - false); - } + get { return GetOptionalTextElement("scriptDisableEditor", false); } } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs index b80b3a8111..56f6d76f49 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs @@ -46,7 +46,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings int UmbracoLibraryCacheDuration { get; } MacroErrorBehaviour MacroErrorBehaviour { get; } - + IEnumerable DisallowedUploadFiles { get; } bool CloneXmlContent { get; } @@ -58,5 +58,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings bool EnableInheritedDocumentTypes { get; } bool EnableInheritedMediaTypes { get; } + + string LoginBackgroundImage { get; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ImagingAutoFillUploadFieldElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ImagingAutoFillUploadFieldElement.cs index 0dfc4afc00..eafe43817d 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ImagingAutoFillUploadFieldElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ImagingAutoFillUploadFieldElement.cs @@ -2,7 +2,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings { - internal class ImagingAutoFillUploadFieldElement : ConfigurationElement, IImagingAutoFillUploadField + internal class ImagingAutoFillUploadFieldElement : UmbracoConfigurationElement, IImagingAutoFillUploadField { /// /// Allow setting internally so we can create a default @@ -17,49 +17,25 @@ namespace Umbraco.Core.Configuration.UmbracoSettings [ConfigurationProperty("widthFieldAlias")] internal InnerTextConfigurationElement WidthFieldAlias { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["widthFieldAlias"], - //set the default - "umbracoWidth"); - } + get { return GetOptionalTextElement("widthFieldAlias", "umbracoWidth"); } } [ConfigurationProperty("heightFieldAlias")] internal InnerTextConfigurationElement HeightFieldAlias { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["heightFieldAlias"], - //set the default - "umbracoHeight"); - } + get { return GetOptionalTextElement("heightFieldAlias", "umbracoHeight"); } } [ConfigurationProperty("lengthFieldAlias")] internal InnerTextConfigurationElement LengthFieldAlias { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["lengthFieldAlias"], - //set the default - "umbracoBytes"); - } + get { return GetOptionalTextElement("lengthFieldAlias", "umbracoBytes"); } } [ConfigurationProperty("extensionFieldAlias")] internal InnerTextConfigurationElement ExtensionFieldAlias { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["extensionFieldAlias"], - //set the default - "umbracoExtension"); - } + get { return GetOptionalTextElement("extensionFieldAlias", "umbracoExtension"); } } string IImagingAutoFillUploadField.Alias diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/LoggingElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/LoggingElement.cs index 8693405e1a..b230641591 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/LoggingElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/LoggingElement.cs @@ -3,67 +3,37 @@ using System.Configuration; namespace Umbraco.Core.Configuration.UmbracoSettings { - internal class LoggingElement : ConfigurationElement, ILoggingSection + internal class LoggingElement : UmbracoConfigurationElement, ILoggingSection { [ConfigurationProperty("autoCleanLogs")] internal InnerTextConfigurationElement AutoCleanLogs { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["autoCleanLogs"], - //set the default - false); - } + get { return GetOptionalTextElement("autoCleanLogs", false); } } [ConfigurationProperty("enableLogging")] internal InnerTextConfigurationElement EnableLogging { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["enableLogging"], - //set the default - true); - } + get { return GetOptionalTextElement("enableLogging", true); } } [ConfigurationProperty("enableAsyncLogging")] internal InnerTextConfigurationElement EnableAsyncLogging { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["enableAsyncLogging"], - //set the default - true); - } + get { return GetOptionalTextElement("enableAsyncLogging", true); } } [ConfigurationProperty("cleaningMiliseconds")] internal InnerTextConfigurationElement CleaningMiliseconds { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["cleaningMiliseconds"], - //set the default - -1); - } + get { return GetOptionalTextElement("cleaningMiliseconds", -1); } } [ConfigurationProperty("maxLogAge")] internal InnerTextConfigurationElement MaxLogAge { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["maxLogAge"], - //set the default - -1); - } + get { return GetOptionalTextElement("maxLogAge", -1); } } [ConfigurationCollection(typeof(DisabledLogTypesCollection), AddItemName = "logTypeAlias")] diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/NotificationsElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/NotificationsElement.cs index 16eb943887..89e3f447ee 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/NotificationsElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/NotificationsElement.cs @@ -2,7 +2,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings { - internal class NotificationsElement : ConfigurationElement + internal class NotificationsElement : UmbracoConfigurationElement { [ConfigurationProperty("email")] internal InnerTextConfigurationElement NotificationEmailAddress @@ -13,13 +13,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings [ConfigurationProperty("disableHtmlEmail")] internal InnerTextConfigurationElement DisableHtmlEmail { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement) this["disableHtmlEmail"], - //set the default - false); - } + get { return GetOptionalTextElement("disableHtmlEmail", false); } } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/RequestHandlerElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/RequestHandlerElement.cs index 9dc4a94824..779d33c8b8 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/RequestHandlerElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/RequestHandlerElement.cs @@ -5,30 +5,18 @@ using System.Collections.Generic; namespace Umbraco.Core.Configuration.UmbracoSettings { - internal class RequestHandlerElement : ConfigurationElement, IRequestHandlerSection + internal class RequestHandlerElement : UmbracoConfigurationElement, IRequestHandlerSection { [ConfigurationProperty("useDomainPrefixes")] public InnerTextConfigurationElement UseDomainPrefixes { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["useDomainPrefixes"], - //set the default - false); - } + get { return GetOptionalTextElement("useDomainPrefixes", false); } } [ConfigurationProperty("addTrailingSlash")] public InnerTextConfigurationElement AddTrailingSlash { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["addTrailingSlash"], - //set the default - true); - } + get { return GetOptionalTextElement("addTrailingSlash", true); } } private UrlReplacingElement _defaultUrlReplacing; diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs index f280b3e20c..ddb168ddbd 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs @@ -2,66 +2,36 @@ namespace Umbraco.Core.Configuration.UmbracoSettings { - internal class SecurityElement : ConfigurationElement, ISecuritySection + internal class SecurityElement : UmbracoConfigurationElement, ISecuritySection { [ConfigurationProperty("keepUserLoggedIn")] internal InnerTextConfigurationElement KeepUserLoggedIn { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["keepUserLoggedIn"], - //set the default - true); - } + get { return GetOptionalTextElement("keepUserLoggedIn", true); } } [ConfigurationProperty("hideDisabledUsersInBackoffice")] internal InnerTextConfigurationElement HideDisabledUsersInBackoffice { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["hideDisabledUsersInBackoffice"], - //set the default - false); - } + get { return GetOptionalTextElement("hideDisabledUsersInBackoffice", false); } } [ConfigurationProperty("allowPasswordReset")] internal InnerTextConfigurationElement AllowPasswordReset { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["allowPasswordReset"], - //set the default - true); - } + get { return GetOptionalTextElement("allowPasswordReset", true); } } [ConfigurationProperty("authCookieName")] internal InnerTextConfigurationElement AuthCookieName { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["authCookieName"], - //set the default - Constants.Web.AuthCookieName); - } + get { return GetOptionalTextElement("authCookieName", Constants.Web.AuthCookieName); } } [ConfigurationProperty("authCookieDomain")] internal InnerTextConfigurationElement AuthCookieDomain { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["authCookieDomain"], - //set the default - null); - } + get { return GetOptionalTextElement("authCookieDomain", null); } } bool ISecuritySection.KeepUserLoggedIn diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/TemplatesElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/TemplatesElement.cs index c7eb659766..4e249df3a2 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/TemplatesElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/TemplatesElement.cs @@ -3,55 +3,31 @@ using System.Configuration; namespace Umbraco.Core.Configuration.UmbracoSettings { - internal class TemplatesElement : ConfigurationElement, ITemplatesSection + internal class TemplatesElement : UmbracoConfigurationElement, ITemplatesSection { [ConfigurationProperty("useAspNetMasterPages")] internal InnerTextConfigurationElement UseAspNetMasterPages { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["useAspNetMasterPages"], - //set the default - true); - } + get { return GetOptionalTextElement("useAspNetMasterPages", true); } } [ConfigurationProperty("enableSkinSupport")] internal InnerTextConfigurationElement EnableSkinSupport { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["enableSkinSupport"], - //set the default - true); - } + get { return GetOptionalTextElement("enableSkinSupport", true); } } [ConfigurationProperty("defaultRenderingEngine", IsRequired = true)] internal InnerTextConfigurationElement DefaultRenderingEngine { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["defaultRenderingEngine"], - //set the default - RenderingEngine.Mvc); - } + get { return GetOptionalTextElement("defaultRenderingEngine", RenderingEngine.Mvc); } } [Obsolete("This has no affect and will be removed in future versions")] [ConfigurationProperty("enableTemplateFolders")] internal InnerTextConfigurationElement EnableTemplateFolders { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["enableTemplateFolders"], - //set the default - false); - } + get { return GetOptionalTextElement("enableTemplateFolders", false); } } bool ITemplatesSection.UseAspNetMasterPages diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoConfigurationElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoConfigurationElement.cs new file mode 100644 index 0000000000..063b5324d8 --- /dev/null +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoConfigurationElement.cs @@ -0,0 +1,36 @@ +using System.Collections.Concurrent; +using System.Configuration; + +namespace Umbraco.Core.Configuration.UmbracoSettings +{ + /// + /// Base class with shared helper methods + /// + internal class UmbracoConfigurationElement : ConfigurationElement + { + /// + /// Used so the RawElement types are not re-created every time they are accessed + /// + private readonly ConcurrentDictionary _rawElements = new ConcurrentDictionary(); + + protected OptionalInnerTextConfigurationElement GetOptionalTextElement(string name, T defaultVal) + { + return (OptionalInnerTextConfigurationElement) _rawElements.GetOrAdd( + name, + s => new OptionalInnerTextConfigurationElement( + (InnerTextConfigurationElement) this[s], + //set the default + defaultVal)); + } + + protected OptionalCommaDelimitedConfigurationElement GetOptionalDelimitedElement(string name, string[] defaultVal) + { + return (OptionalCommaDelimitedConfigurationElement) _rawElements.GetOrAdd( + name, + s => new OptionalCommaDelimitedConfigurationElement( + (CommaDelimitedConfigurationElement) this[name], + //set the default + defaultVal)); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-Applications.cs b/src/Umbraco.Core/Constants-Applications.cs index aa3725b819..d2a836863e 100644 --- a/src/Umbraco.Core/Constants-Applications.cs +++ b/src/Umbraco.Core/Constants-Applications.cs @@ -141,7 +141,5 @@ //TODO: Fill in the rest! } - } - - + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-DatabaseProviders.cs b/src/Umbraco.Core/Constants-DatabaseProviders.cs new file mode 100644 index 0000000000..2a64ade081 --- /dev/null +++ b/src/Umbraco.Core/Constants-DatabaseProviders.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Core +{ + public static partial class Constants + { + public static class DatabaseProviders + { + public const string SqlCe = "System.Data.SqlServerCe.4.0"; + public const string SqlServer = "System.Data.SqlClient"; + public const string MySql = "MySql.Data.MySqlClient"; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-DeploySelector.cs b/src/Umbraco.Core/Constants-DeploySelector.cs new file mode 100644 index 0000000000..cd9c48a6f5 --- /dev/null +++ b/src/Umbraco.Core/Constants-DeploySelector.cs @@ -0,0 +1,17 @@ +namespace Umbraco.Core +{ + public static partial class Constants + { + /// + /// Contains the valid selector values. + /// + public static class DeploySelector + { + public const string This = "this"; + public const string ThisAndChildren = "this-and-children"; + public const string ThisAndDescendants = "this-and-descendants"; + public const string ChildrenOfThis = "children"; + public const string DescendantsOfThis = "descendants"; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-ObjectTypes.cs b/src/Umbraco.Core/Constants-ObjectTypes.cs index 29c083b4a2..6af011d4bd 100644 --- a/src/Umbraco.Core/Constants-ObjectTypes.cs +++ b/src/Umbraco.Core/Constants-ObjectTypes.cs @@ -117,13 +117,18 @@ namespace Umbraco.Core /// /// Guid for a Member object. /// - public static readonly Guid MemberGuid = new Guid("39EB0F98-B348-42A1-8662-E7EB18487560"); + public static readonly Guid MemberGuid = new Guid(Member); /// /// Guid for a Member Group object. /// public const string MemberGroup = "366E63B9-880F-4E13-A61C-98069B029728"; + /// + /// Guid for a Member Group object. + /// + public static readonly Guid MemberGroupGuid = new Guid(MemberGroup); + /// /// Guid for a Member Type object. /// @@ -153,6 +158,11 @@ namespace Umbraco.Core /// public const string Template = "6FBDE604-4178-42CE-A10B-8A2600A2F07D"; + /// + /// Guid for a Template object. + /// + public static readonly Guid TemplateTypeGuid = new Guid(Template); + /// /// Guid for a Lock object. /// @@ -162,6 +172,46 @@ namespace Umbraco.Core /// Guid for a Lock object. /// public static readonly Guid LockObjectGuid = new Guid(LockObject); + + /// + /// Guid for a relation type. + /// + public const string RelationType = "B1988FAD-8675-4F47-915A-B3A602BC5D8D"; + + /// + /// Guid for a relation type. + /// + public static readonly Guid RelationTypeGuid = new Guid(RelationType); + + /// + /// Guid for a Forms Form. + /// + public const string FormsForm = "F5A9F787-6593-46F0-B8FF-BFD9BCA9F6BB"; + + /// + /// Guid for a Forms Form. + /// + public static readonly Guid FormsFormGuid = new Guid(FormsForm); + + /// + /// Guid for a Forms PreValue Source. + /// + public const string FormsPreValue = "42D7BF9B-A362-4FEE-B45A-674D5C064B70"; + + /// + /// Guid for a Forms PreValue Source. + /// + public static readonly Guid FormsPreValueGuid = new Guid(FormsPreValue); + + /// + /// Guid for a Forms DataSource. + /// + public const string FormsDataSource = "CFED6CE4-9359-443E-9977-9956FEB1D867"; + + /// + /// Guid for a Forms DataSource. + /// + public static readonly Guid FormsDataSourceGuid = new Guid(FormsDataSource); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index 9ac8d7a4be..ddec54b2dd 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -4,6 +4,8 @@ namespace Umbraco.Core { public static partial class Constants { + // fixme - kill the Whatever2! + /// /// Defines the identifiers for Umbraco Property Editors as constants for easy centralized access/management. /// @@ -42,10 +44,14 @@ namespace Umbraco.Core [Obsolete("GUIDs are no longer used to reference Property Editors, use the Alias constant instead. This will be removed in future versions")] public const string ContentPicker = "158AA029-24ED-4948-939E-C3DA209E5FBA"; + + [Obsolete("This is an obsoleted content picker, use ContentPicker2Alias instead")] + public const string ContentPickerAlias = "Umbraco.ContentPickerAlias"; + /// /// Alias for the Content Picker datatype. /// - public const string ContentPickerAlias = "Umbraco.ContentPickerAlias"; + public const string ContentPicker2Alias = "Umbraco.ContentPicker2"; /// /// Guid for the Date datatype. @@ -192,11 +198,15 @@ namespace Umbraco.Core [Obsolete("GUIDs are no longer used to reference Property Editors, use the Alias constant instead. This will be removed in future versions")] public const string MediaPicker = "EAD69342-F06D-4253-83AC-28000225583B"; + [Obsolete("This is an obsoleted picker, use MediaPicker2Alias instead")] + public const string MediaPickerAlias = "Umbraco.MediaPicker"; + /// /// Alias for the Media Picker datatype. /// - public const string MediaPickerAlias = "Umbraco.MediaPicker"; + public const string MediaPicker2Alias = "Umbraco.MediaPicker2"; + [Obsolete("This is an obsoleted picker, use MediaPicker2Alias instead")] public const string MultipleMediaPickerAlias = "Umbraco.MultipleMediaPicker"; /// @@ -205,10 +215,13 @@ namespace Umbraco.Core [Obsolete("GUIDs are no longer used to reference Property Editors, use the Alias constant instead. This will be removed in future versions")] public const string MemberPicker = "39F533E4-0551-4505-A64B-E0425C5CE775"; + [Obsolete("This is an obsoleted picker, use MemberPicker2Alias instead")] + public const string MemberPickerAlias = "Umbraco.MemberPicker"; + /// /// Alias for the Member Picker datatype. /// - public const string MemberPickerAlias = "Umbraco.MemberPicker"; + public const string MemberPicker2Alias = "Umbraco.MemberPicker2"; /// /// Alias for the Member Group Picker datatype. @@ -221,10 +234,13 @@ namespace Umbraco.Core [Obsolete("GUIDs are no longer used to reference Property Editors, use the Alias constant instead. This will be removed in future versions")] public const string MultiNodeTreePicker = "7E062C13-7C41-4AD9-B389-41D88AEEF87C"; + [Obsolete("This is an obsoleted picker, use MultiNodeTreePicker2Alias instead")] + public const string MultiNodeTreePickerAlias = "Umbraco.MultiNodeTreePicker"; + /// /// Alias for the Multi-Node Tree Picker datatype /// - public const string MultiNodeTreePickerAlias = "Umbraco.MultiNodeTreePicker"; + public const string MultiNodeTreePicker2Alias = "Umbraco.MultiNodeTreePicker2"; /// /// Guid for the Multiple Textstring datatype. @@ -276,11 +292,14 @@ namespace Umbraco.Core [Obsolete("GUIDs are no longer used to reference Property Editors, use the Alias constant instead. This will be removed in future versions")] public const string RelatedLinks = "71B8AD1A-8DC2-425C-B6B8-FAA158075E63"; - /// - /// Alias for the Related Links datatype. - /// + [Obsolete("This is an obsoleted picker, use RelatedLinks2Alias instead")] public const string RelatedLinksAlias = "Umbraco.RelatedLinks"; + /// + /// Alias for the Related Links property editor. + /// + public const string RelatedLinks2Alias = "Umbraco.RelatedLinks2"; + /// /// Guid for the Slider datatype. /// diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs new file mode 100644 index 0000000000..bd2e1c5acf --- /dev/null +++ b/src/Umbraco.Core/Constants-Security.cs @@ -0,0 +1,33 @@ +using System; +using System.ComponentModel; + +namespace Umbraco.Core +{ + public static partial class Constants + { + public static class Security + { + + public const string BackOfficeAuthenticationType = "UmbracoBackOffice"; + public const string BackOfficeExternalAuthenticationType = "UmbracoExternalCookie"; + public const string BackOfficeExternalCookieName = "UMB_EXTLOGIN"; + public const string BackOfficeTokenAuthenticationType = "UmbracoBackOfficeToken"; + public const string BackOfficeTwoFactorAuthenticationType = "UmbracoTwoFactorCookie"; + + /// + /// The prefix used for external identity providers for their authentication type + /// + /// + /// By default we don't want to interfere with front-end external providers and their default setup, for back office the + /// providers need to be setup differently and each auth type for the back office will be prefixed with this value + /// + public const string BackOfficeExternalAuthenticationTypePrefix = "Umbraco."; + + public const string StartContentNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startcontentnode"; + public const string StartMediaNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startmedianode"; + public const string AllowedApplicationsClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/allowedapp"; + public const string SessionIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/sessionid"; + + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-System.cs b/src/Umbraco.Core/Constants-System.cs index 3c1a34fee7..74ffcef8fc 100644 --- a/src/Umbraco.Core/Constants-System.cs +++ b/src/Umbraco.Core/Constants-System.cs @@ -21,13 +21,10 @@ /// The integer identifier for media's recycle bin. /// public const int RecycleBinMedia = -21; - } - public static class DatabaseProviders - { - public const string SqlCe = "System.Data.SqlServerCe.4.0"; - public const string SqlServer = "System.Data.SqlClient"; - public const string MySql = "MySql.Data.MySqlClient"; + public const string UmbracoConnectionName = "umbracoDbDSN"; + public const string UmbracoMigrationName = "Umbraco"; } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index 22df82367d..e299fa507a 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -28,36 +28,6 @@ namespace Umbraco.Core [EditorBrowsable(EditorBrowsableState.Never)] public const string AuthCookieName = "UMB_UCONTEXT"; - } - - public static class Security - { - - public const string BackOfficeAuthenticationType = "UmbracoBackOffice"; - public const string BackOfficeExternalAuthenticationType = "UmbracoExternalCookie"; - public const string BackOfficeExternalCookieName = "UMB_EXTLOGIN"; - public const string BackOfficeTokenAuthenticationType = "UmbracoBackOfficeToken"; - public const string BackOfficeTwoFactorAuthenticationType = "UmbracoTwoFactorCookie"; - - /// - /// The prefix used for external identity providers for their authentication type - /// - /// - /// By default we don't want to interfere with front-end external providers and their default setup, for back office the - /// providers need to be setup differently and each auth type for the back office will be prefixed with this value - /// - public const string BackOfficeExternalAuthenticationTypePrefix = "Umbraco."; - - public const string StartContentNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startcontentnode"; - public const string StartMediaNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startmedianode"; - public const string AllowedApplicationsClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/allowedapp"; - public const string SessionIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/sessionid"; - - } - - public static class IO - { - public const string MediaFileSystemProvider = "media"; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/CoreRuntime.cs b/src/Umbraco.Core/CoreRuntime.cs index 5655d7b0b8..5c4841a616 100644 --- a/src/Umbraco.Core/CoreRuntime.cs +++ b/src/Umbraco.Core/CoreRuntime.cs @@ -18,6 +18,7 @@ using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Plugins; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; namespace Umbraco.Core @@ -224,13 +225,12 @@ namespace Umbraco.Core // register database factory - required to check for migrations // will be initialized with syntax providers and a logger, and will try to configure // from the default connection string name, if possible, else will remain non-configured - // until the database context configures it properly (eg when installing) + // until properly configured (eg when installing) container.RegisterSingleton(); - // register a database accessor - required by database factory - // will be replaced by HybridUmbracoDatabaseAccessor in the web runtime - // fixme - we should NOT be using thread static at all + will NOT get replaced = wtf? - container.RegisterSingleton(); + // register the scope provider + container.RegisterSingleton(); + container.RegisterSingleton(f => f.GetInstance()); // register MainDom container.RegisterSingleton(); @@ -342,7 +342,7 @@ namespace Umbraco.Core var sql = databaseFactory.Sql() .Select() .From() - .Where(x => x.Name.InvariantEquals(GlobalSettings.UmbracoMigrationName) && x.Version == codeVersionString); + .Where(x => x.Name.InvariantEquals(Constants.System.UmbracoMigrationName) && x.Version == codeVersionString); return database.FirstOrDefault(sql) != null; } } diff --git a/src/Umbraco.Core/CoreRuntimeComponent.cs b/src/Umbraco.Core/CoreRuntimeComponent.cs index 4c9c6b1001..520efef53a 100644 --- a/src/Umbraco.Core/CoreRuntimeComponent.cs +++ b/src/Umbraco.Core/CoreRuntimeComponent.cs @@ -18,6 +18,7 @@ using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Migrations; using Umbraco.Core.Plugins; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Core.Sync; @@ -94,7 +95,8 @@ namespace Umbraco.Core composition.Container.RegisterSingleton(factory => new DatabaseServerMessenger( factory.GetInstance(), - factory.GetInstance(), + factory.GetInstance(), + factory.GetInstance(), factory.GetInstance(), factory.GetInstance(), true, new DatabaseServerMessengerOptions())); diff --git a/src/Umbraco.Core/DI/Current.cs b/src/Umbraco.Core/DI/Current.cs index bdfb34c48b..b0fff220da 100644 --- a/src/Umbraco.Core/DI/Current.cs +++ b/src/Umbraco.Core/DI/Current.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Persistence; using Umbraco.Core.Plugins; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Core.Sync; @@ -147,8 +148,11 @@ namespace Umbraco.Core.DI public static ServiceContext Services => Container.GetInstance(); - public static IUmbracoDatabaseFactory DatabaseFactory - => Container.GetInstance(); + public static IScopeProvider ScopeProvider + => Container.GetInstance(); + + public static IDatabaseContext DatabaseContext + => Container.GetInstance(); #endregion } diff --git a/src/Umbraco.Core/DI/LightInjectExtensions.cs b/src/Umbraco.Core/DI/LightInjectExtensions.cs index 2f5c4208ea..1f53d0e0c8 100644 --- a/src/Umbraco.Core/DI/LightInjectExtensions.cs +++ b/src/Umbraco.Core/DI/LightInjectExtensions.cs @@ -3,9 +3,13 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using LightInject; +using Umbraco.Core.Exceptions; namespace Umbraco.Core.DI { + /// + /// Provides extensions to LightInject. + /// internal static class LightInjectExtensions { /// @@ -34,10 +38,10 @@ namespace Umbraco.Core.DI // ensure that we do *not* scan assemblies // we explicitely RegisterFrom our own composition roots and don't want them scanned - container.AssemblyScanner = new AssemblyScanner(container.AssemblyScanner); + container.AssemblyScanner = new AssemblyScanner(/*container.AssemblyScanner*/); - // see notes in MixedScopeManagerProvider - container.ScopeManagerProvider = new MixedScopeManagerProvider(); + // see notes in MixedLightInjectScopeManagerProvider + container.ScopeManagerProvider = new MixedLightInjectScopeManagerProvider(); // self-register container.Register(_ => container); @@ -48,12 +52,12 @@ namespace Umbraco.Core.DI private class AssemblyScanner : IAssemblyScanner { - private readonly IAssemblyScanner _scanner; + //private readonly IAssemblyScanner _scanner; - public AssemblyScanner(IAssemblyScanner scanner) - { - _scanner = scanner; - } + //public AssemblyScanner(IAssemblyScanner scanner) + //{ + // _scanner = scanner; + //} public void Scan(Assembly assembly, IServiceRegistry serviceRegistry, Func lifetime, Func shouldRegister) { @@ -66,6 +70,50 @@ namespace Umbraco.Core.DI } } + /// + /// Registers a service implementation with a specified lifetime. + /// + /// The type of the service. + /// The type of the implementation. + /// The type of the lifetime. + /// The container. + public static void Register(this IServiceContainer container) + where TImplementation : TService + where TLifetime : ILifetime, new() + { + container.Register(new TLifetime()); + } + + /// + /// Registers a service implementation with a specified lifetime. + /// + /// The type of the service. + /// The type of the lifetime. + /// The container. + /// A factory. + public static void Register(this IServiceContainer container, Func factory) + where TLifetime : ILifetime, new() + { + container.Register(factory, new TLifetime()); + } + + /// + /// Registers several service implementations with a specified lifetime. + /// + /// The type of the service. + /// The type of the lifetime. + /// The container. + /// The types of the implementations. + public static void RegisterMany(this IServiceContainer container, IEnumerable implementations) + where TLifeTime : ILifetime, new() + { + foreach (var implementation in implementations) + { + // if "typeof (TService)" is there then "implementation.FullName" MUST be there too + container.Register(typeof(TService), implementation, implementation.FullName, new TLifeTime()); + } + } + /// /// Registers the TService with the factory that describes the dependencies of the service, as a singleton. /// @@ -73,40 +121,54 @@ namespace Umbraco.Core.DI { var registration = container.GetAvailableService(serviceName); if (registration == null) + { container.Register(factory, serviceName, new PerContainerLifetime()); + } else - container.UpdateRegistration(registration, null, factory); + { + if (registration.Lifetime is PerContainerLifetime == false) + throw new InvalidOperationException("Existing registration lifetime is not PerContainer."); + UpdateRegistration(registration, null, factory); + } } /// /// Registers the TService with the TImplementation as a singleton. /// - /// - /// - /// public static void RegisterSingleton(this IServiceRegistry container) where TImplementation : TService { var registration = container.GetAvailableService(); if (registration == null) + { container.Register(new PerContainerLifetime()); + } else - container.UpdateRegistration(registration, typeof(TImplementation), null); + { + if (registration.Lifetime is PerContainerLifetime == false) + throw new InvalidOperationException("Existing registration lifetime is not PerContainer."); + UpdateRegistration(registration, typeof(TImplementation), null); + } } /// /// Registers a concrete type as a singleton service. /// - /// - /// public static void RegisterSingleton(this IServiceRegistry container) { var registration = container.GetAvailableService(); if (registration == null) + { container.Register(new PerContainerLifetime()); + } else - container.UpdateRegistration(registration, typeof(TImplementation), null); + { + if (registration.Lifetime is PerContainerLifetime == false) + throw new InvalidOperationException("Existing registration lifetime is not PerContainer."); + UpdateRegistration(registration, typeof(TImplementation), null); + } + } /// @@ -121,7 +183,7 @@ namespace Umbraco.Core.DI if (registration == null) container.Register(factory, new PerContainerLifetime()); else - container.UpdateRegistration(registration, null, factory); + UpdateRegistration(registration, null, factory); } // fixme - what's below ALSO applies to non-singleton ie transient services @@ -139,7 +201,7 @@ namespace Umbraco.Core.DI // all in all, not sure we want to let ppl have direct access to the container // we might instead want to expose some methods in UmbracoComponentBase or whatever? - private static void UpdateRegistration(this IServiceRegistry container, ServiceRegistration registration, Type implementingType, Delegate factoryExpression) + private static void UpdateRegistration(Registration registration, Type implementingType, Delegate factoryExpression) { // if the container has compiled already then the registrations have been captured, // and re-registering - although updating available services - does not modify the @@ -167,25 +229,69 @@ namespace Umbraco.Core.DI // }); } + /// + /// Gets the available service registrations for a service type. + /// + /// The service type. + /// The container. + /// The service registrations for the service type. public static IEnumerable GetAvailableServices(this IServiceRegistry container) { var typeofTService = typeof(TService); return container.AvailableServices.Where(x => x.ServiceType == typeofTService); } + /// + /// Gets the unique available service registration for a service type. + /// + /// The service type. + /// The container. + /// The unique service registration for the service type. + /// Can return null, but throws if more than one registration exist for the service type. public static ServiceRegistration GetAvailableService(this IServiceRegistry container) { var typeofTService = typeof(TService); return container.AvailableServices.SingleOrDefault(x => x.ServiceType == typeofTService); } + /// + /// Gets the unique available service registration for a service type and a name. + /// + /// The service type. + /// The container. + /// The name. + /// The unique service registration for the service type and the name. + /// Can return null, but throws if more than one registration exist for the service type and the name. public static ServiceRegistration GetAvailableService(this IServiceRegistry container, string name) { var typeofTService = typeof(TService); return container.AvailableServices.SingleOrDefault(x => x.ServiceType == typeofTService && x.ServiceName == name); } + /// + /// Gets an instance of a TService or throws a meaningful exception. + /// + /// The service type. + /// The container. + /// The instance. + public static TService GetInstanceOrThrow(this IServiceFactory factory) + { + if (factory == null) + throw new ArgumentNullException(nameof(factory)); + + try + { + return factory.GetInstance(); + } + catch (Exception e) + { + LightInjectException.TryThrow(e); + throw; + } + } + // FIXME or just use names?! + // this is what RegisterMany does => kill RegisterCollection! /// /// In order for LightInject to deal with enumerables of the same type, each one needs to be registered as their explicit types @@ -247,7 +353,9 @@ namespace Umbraco.Core.DI { container.RegisterFallback((serviceType, serviceName) => { + //Current.Logger.Debug(typeof(LightInjectExtensions), $"Fallback for type {serviceType.FullName}."); // https://github.com/seesharper/LightInject/issues/173 + if (typeof(T).IsAssignableFrom(serviceType)) container.Register(serviceType); return false; @@ -269,6 +377,7 @@ namespace Umbraco.Core.DI { //Current.Logger.Debug(typeof(LightInjectExtensions), $"Fallback for type {serviceType.FullName}."); // https://github.com/seesharper/LightInject/issues/173 + if (type.IsAssignableFromGtd(serviceType)) container.Register(serviceType); return false; diff --git a/src/Umbraco.Core/DI/MixedScopeManagerProvider.cs b/src/Umbraco.Core/DI/MixedLightInjectScopeManagerProvider.cs similarity index 86% rename from src/Umbraco.Core/DI/MixedScopeManagerProvider.cs rename to src/Umbraco.Core/DI/MixedLightInjectScopeManagerProvider.cs index 4bfec9dd1a..e1debca23f 100644 --- a/src/Umbraco.Core/DI/MixedScopeManagerProvider.cs +++ b/src/Umbraco.Core/DI/MixedLightInjectScopeManagerProvider.cs @@ -5,7 +5,7 @@ namespace Umbraco.Core.DI { // by default, the container's scope manager provider is PerThreadScopeManagerProvider, // and then container.EnablePerWebRequestScope() replaces it with PerWebRequestScopeManagerProvider, - // however if any delate has been compiled already at that point, it captures the scope + // however if any delegate has been compiled already at that point, it captures the scope // manager provider and changing it afterwards has no effect for that delegate. // // therefore, Umbraco uses the mixed scope manager provider, which initially wraps an instance @@ -17,11 +17,11 @@ namespace Umbraco.Core.DI // when doing eg container.EnableMvc() or anything that does container.EnablePerWebRequestScope() // we need to take great care to preserve the mixed scope manager provider! - public class MixedScopeManagerProvider : IScopeManagerProvider + public class MixedLightInjectScopeManagerProvider : IScopeManagerProvider { private IScopeManagerProvider _provider; - public MixedScopeManagerProvider() + public MixedLightInjectScopeManagerProvider() { _provider = new PerThreadScopeManagerProvider(); } diff --git a/src/Umbraco.Core/DI/RepositoryCompositionRoot.cs b/src/Umbraco.Core/DI/RepositoryCompositionRoot.cs index dd8715d06e..e1ce63153e 100644 --- a/src/Umbraco.Core/DI/RepositoryCompositionRoot.cs +++ b/src/Umbraco.Core/DI/RepositoryCompositionRoot.cs @@ -16,9 +16,8 @@ namespace Umbraco.Core.DI public void Compose(IServiceRegistry container) { - // register IUnitOfWork providers - container.RegisterSingleton(); - container.RegisterSingleton(); + // register the IUnitOfWork provider + container.RegisterSingleton(); // register repository factory container.RegisterSingleton(); diff --git a/src/Umbraco.Core/DatabaseBuilder.cs b/src/Umbraco.Core/DatabaseBuilder.cs index a43c50ec37..626607f9a4 100644 --- a/src/Umbraco.Core/DatabaseBuilder.cs +++ b/src/Umbraco.Core/DatabaseBuilder.cs @@ -13,6 +13,7 @@ using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Migrations; using Umbraco.Core.Persistence.Migrations.Initial; using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; namespace Umbraco.Core @@ -23,24 +24,22 @@ namespace Umbraco.Core public class DatabaseBuilder { private readonly IUmbracoDatabaseFactory _databaseFactory; + private readonly IScopeProvider _scopeProvider; private readonly IRuntimeState _runtime; private readonly IMigrationEntryService _migrationEntryService; private readonly ILogger _logger; private DatabaseSchemaResult _databaseSchemaValidationResult; - public DatabaseBuilder(IUmbracoDatabaseFactory databaseFactory, IRuntimeState runtime, IMigrationEntryService migrationEntryService, ILogger logger) + public DatabaseBuilder(IScopeProvider scopeProvider, IUmbracoDatabaseFactory databaseFactory, IRuntimeState runtime, IMigrationEntryService migrationEntryService, ILogger logger) { + _scopeProvider = scopeProvider; _databaseFactory = databaseFactory; _runtime = runtime; _migrationEntryService = migrationEntryService; _logger = logger; } - public IUmbracoDatabase Database => _databaseFactory.GetDatabase(); - - public ISqlSyntaxProvider SqlSyntax => _databaseFactory.SqlSyntax; - #region Status /// @@ -89,6 +88,25 @@ namespace Umbraco.Core return DbConnectionExtensions.IsConnectionAvailable(connectionString, providerName); } + public bool HasSomeNonDefaultUser() + { + using (var scope = _scopeProvider.CreateScope()) + { + // look for the default user with default password + var result = scope.Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoUser WHERE id=0 AND userPassword='default'"); + var has = result != 1; + if (has == false) + { + // found only 1 user == the default user with default password + // however this always exists on uCloud, also need to check if there are other users too + result = scope.Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoUser"); + has = result != 1; + } + scope.Complete(); + return has; + } + } + #endregion #region Configure Connection String @@ -269,7 +287,7 @@ namespace Umbraco.Core if (string.IsNullOrWhiteSpace(providerName)) throw new ArgumentNullOrEmptyException(nameof(providerName)); // set the connection string for the new datalayer - var connectionStringSettings = new ConnectionStringSettings(GlobalSettings.UmbracoConnectionName, connectionString, providerName); + var connectionStringSettings = new ConnectionStringSettings(Constants.System.UmbracoConnectionName, connectionString, providerName); var fileName = IOHelper.MapPath($"{SystemDirectories.Root}/web.config"); var xml = XDocument.Load(fileName, LoadOptions.PreserveWhitespace); @@ -278,11 +296,11 @@ namespace Umbraco.Core if (connectionStrings == null) throw new Exception("Invalid web.config file."); // update connectionString if it exists, or else create a new connectionString - var setting = connectionStrings.Descendants("add").FirstOrDefault(s => s.Attribute("name").Value == GlobalSettings.UmbracoConnectionName); + var setting = connectionStrings.Descendants("add").FirstOrDefault(s => s.Attribute("name").Value == Constants.System.UmbracoConnectionName); if (setting == null) { connectionStrings.Add(new XElement("add", - new XAttribute("name", GlobalSettings.UmbracoConnectionName), + new XAttribute("name", Constants.System.UmbracoConnectionName), new XAttribute("connectionString", connectionStringSettings), new XAttribute("providerName", providerName))); } @@ -332,7 +350,7 @@ namespace Umbraco.Core internal static void GiveLegacyAChance(IUmbracoDatabaseFactory factory, ILogger logger) { // look for the legacy appSettings key - var legacyConnString = ConfigurationManager.AppSettings[GlobalSettings.UmbracoConnectionName]; + var legacyConnString = ConfigurationManager.AppSettings[Constants.System.UmbracoConnectionName]; if (string.IsNullOrWhiteSpace(legacyConnString)) return; var test = legacyConnString.ToLowerInvariant(); @@ -368,7 +386,7 @@ namespace Umbraco.Core } // remove the legacy connection string, so we don't end up in a loop if something goes wrong - GlobalSettings.RemoveSetting(GlobalSettings.UmbracoConnectionName); + GlobalSettings.RemoveSetting(Constants.System.UmbracoConnectionName); } #endregion @@ -376,6 +394,16 @@ namespace Umbraco.Core #region Database Schema internal DatabaseSchemaResult ValidateDatabaseSchema() + { + using (var scope = _scopeProvider.CreateScope()) + { + var result = ValidateDatabaseSchema(scope); + scope.Complete(); + return result; + } + } + + private DatabaseSchemaResult ValidateDatabaseSchema(IScope scope) { if (_databaseFactory.Configured == false) return new DatabaseSchemaResult(_databaseFactory.SqlSyntax); @@ -383,13 +411,24 @@ namespace Umbraco.Core if (_databaseSchemaValidationResult != null) return _databaseSchemaValidationResult; - var database = _databaseFactory.GetDatabase(); + var database = scope.Database; var dbSchema = new DatabaseSchemaCreation(database, _logger); _databaseSchemaValidationResult = dbSchema.ValidateSchema(); + scope.Complete(); return _databaseSchemaValidationResult; } internal Result CreateDatabaseSchemaAndData() + { + using (var scope = _scopeProvider.CreateScope()) + { + var result = CreateDatabaseSchemaAndData(scope); + scope.Complete(); + return result; + } + } + + private Result CreateDatabaseSchemaAndData(IScope scope) { try { @@ -401,7 +440,7 @@ namespace Umbraco.Core _logger.Info("Database configuration status: Started"); - var database = _databaseFactory.GetDatabase(); + var database = scope.Database; // If MySQL, we're going to ensure that database calls are maintaining proper casing as to remove the necessity for checks // for case insensitive queries. In an ideal situation (which is what we're striving for), all calls would be case sensitive. @@ -455,11 +494,18 @@ namespace Umbraco.Core } } - /// - /// This assumes all of the previous checks are done! - /// - /// internal Result UpgradeSchemaAndData(IMigrationEntryService migrationEntryService, MigrationCollectionBuilder builder) + { + using (var scope = _scopeProvider.CreateScope()) + { + var result = UpgradeSchemaAndData(scope, migrationEntryService, builder); + scope.Complete(); + return result; + } + } + + // This assumes all of the previous checks are done! + private Result UpgradeSchemaAndData(IScope scope, IMigrationEntryService migrationEntryService, MigrationCollectionBuilder builder) { try { @@ -471,7 +517,7 @@ namespace Umbraco.Core _logger.Info("Database upgrade started"); - var database = _databaseFactory.GetDatabase(); + var database = scope.Database; //var supportsCaseInsensitiveQueries = SqlSyntax.SupportsCaseInsensitiveQueries(database); var message = GetResultMessageForMySql(); @@ -511,7 +557,7 @@ namespace Umbraco.Core //DO the upgrade! - var runner = new MigrationRunner(builder, migrationEntryService, _logger, currentInstalledVersion, UmbracoVersion.SemanticVersion, GlobalSettings.UmbracoMigrationName); + var runner = new MigrationRunner(builder, migrationEntryService, _logger, currentInstalledVersion, UmbracoVersion.SemanticVersion, Constants.System.UmbracoMigrationName); var migrationContext = new MigrationContext(database, _logger); var upgraded = runner.Execute(migrationContext /*, true*/); diff --git a/src/Umbraco.Core/Deploy/ArtifactBase.cs b/src/Umbraco.Core/Deploy/ArtifactBase.cs new file mode 100644 index 0000000000..a3fd160a49 --- /dev/null +++ b/src/Umbraco.Core/Deploy/ArtifactBase.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace Umbraco.Core.Deploy +{ + /// + /// Provides a base class to all artifacts. + /// + public abstract class ArtifactBase : IArtifact + where TUdi : Udi + { + protected ArtifactBase(TUdi udi, IEnumerable dependencies = null) + { + if (udi == null) + throw new ArgumentNullException("udi"); + Udi = udi; + Name = Udi.ToString(); + + Dependencies = dependencies ?? Enumerable.Empty(); + _checksum = new Lazy(GetChecksum); + } + + private readonly Lazy _checksum; + private IEnumerable _dependencies; + + protected abstract string GetChecksum(); + + #region Abstract implementation of IArtifactSignature + + Udi IArtifactSignature.Udi + { + get { return Udi; } + } + + public TUdi Udi { get; set; } + + [JsonIgnore] + public string Checksum + { + get { return _checksum.Value; } + } + + public IEnumerable Dependencies + { + get { return _dependencies; } + set { _dependencies = value.OrderBy(x => x.Udi); } + } + + #endregion + + public string Name { get; set; } + public string Alias { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/ArtifactDependency.cs b/src/Umbraco.Core/Deploy/ArtifactDependency.cs new file mode 100644 index 0000000000..41e349d636 --- /dev/null +++ b/src/Umbraco.Core/Deploy/ArtifactDependency.cs @@ -0,0 +1,40 @@ +namespace Umbraco.Core.Deploy +{ + /// + /// Represents an artifact dependency. + /// + /// + /// Dependencies have an order property which indicates whether it must be respected when ordering artifacts. + /// Dependencies have a mode which can be Match or Exist depending on whether the checksum should match. + /// + public class ArtifactDependency + { + /// + /// Initializes a new instance of the ArtifactDependency class with an entity identifier and a mode. + /// + /// The entity identifier of the artifact that is a dependency. + /// A value indicating whether the dependency is ordering. + /// The dependency mode. + public ArtifactDependency(Udi udi, bool ordering, ArtifactDependencyMode mode) + { + Udi = udi; + Ordering = ordering; + Mode = mode; + } + + /// + /// Gets the entity id of the artifact that is a dependency. + /// + public Udi Udi { get; private set; } + + /// + /// Gets a value indicating whether the dependency is ordering. + /// + public bool Ordering { get; private set; } + + /// + /// Gets the dependency mode. + /// + public ArtifactDependencyMode Mode { get; private set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs b/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs new file mode 100644 index 0000000000..fd036f4628 --- /dev/null +++ b/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Umbraco.Core.Deploy +{ + /// + /// Represents a collection of distinct . + /// + /// The collection cannot contain duplicates and modes are properly managed. + public class ArtifactDependencyCollection : ICollection + { + private readonly Dictionary _dependencies + = new Dictionary(); + + public IEnumerator GetEnumerator() + { + return _dependencies.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void Add(ArtifactDependency item) + { + if (_dependencies.ContainsKey(item.Udi)) + { + var exist = _dependencies[item.Udi]; + if (item.Mode == ArtifactDependencyMode.Exist || item.Mode == exist.Mode) + return; + } + + _dependencies[item.Udi] = item; + } + + public void Clear() + { + _dependencies.Clear(); + } + + public bool Contains(ArtifactDependency item) + { + return _dependencies.ContainsKey(item.Udi) && + (_dependencies[item.Udi].Mode == item.Mode || _dependencies[item.Udi].Mode == ArtifactDependencyMode.Match); + } + + public void CopyTo(ArtifactDependency[] array, int arrayIndex) + { + _dependencies.Values.CopyTo(array, arrayIndex); + } + + public bool Remove(ArtifactDependency item) + { + throw new NotSupportedException(); + } + + public int Count + { + get { return _dependencies.Count; } + } + + public bool IsReadOnly + { + get { return false; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/ArtifactDependencyMode.cs b/src/Umbraco.Core/Deploy/ArtifactDependencyMode.cs new file mode 100644 index 0000000000..7ee5c2f220 --- /dev/null +++ b/src/Umbraco.Core/Deploy/ArtifactDependencyMode.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Core.Deploy +{ + /// + /// Indicates the mode of the dependency. + /// + public enum ArtifactDependencyMode + { + /// + /// The dependency must match exactly. + /// + Match, + + /// + /// The dependency must exist. + /// + Exist + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/ArtifactDeployState.cs b/src/Umbraco.Core/Deploy/ArtifactDeployState.cs new file mode 100644 index 0000000000..3723e483cb --- /dev/null +++ b/src/Umbraco.Core/Deploy/ArtifactDeployState.cs @@ -0,0 +1,50 @@ +namespace Umbraco.Core.Deploy +{ + /// + /// Represent the state of an artifact being deployed. + /// + public abstract class ArtifactDeployState + { + /// + /// Creates a new instance of the class from an artifact and an entity. + /// + /// The type of the artifact. + /// The type of the entity. + /// The artifact. + /// The entity. + /// The service connector deploying the artifact. + /// The next pass number. + /// A deploying artifact. + public static ArtifactDeployState Create(TArtifact art, TEntity entity, IServiceConnector connector, int nextPass) + where TArtifact : IArtifact + { + return new ArtifactDeployState(art, entity, connector, nextPass); + } + + /// + /// Gets the artifact. + /// + public IArtifact Artifact + { + get { return GetArtifactAsIArtifact(); } + } + + /// + /// Gets the artifact as an . + /// + /// The artifact, as an . + /// This is because classes that inherit from this class cannot override the Artifact property + /// with a property that specializes the return type, and so they need to 'new' the property. + protected abstract IArtifact GetArtifactAsIArtifact(); + + /// + /// Gets or sets the service connector in charge of deploying the artifact. + /// + public IServiceConnector Connector { get; set; } + + /// + /// Gets or sets the next pass number. + /// + public int NextPass { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/ArtifactDeployStateOfTArtifactTEntity.cs b/src/Umbraco.Core/Deploy/ArtifactDeployStateOfTArtifactTEntity.cs new file mode 100644 index 0000000000..b4d2be23cd --- /dev/null +++ b/src/Umbraco.Core/Deploy/ArtifactDeployStateOfTArtifactTEntity.cs @@ -0,0 +1,48 @@ +namespace Umbraco.Core.Deploy +{ + /// + /// Represent the state of an artifact being deployed. + /// + /// The type of the artifact. + /// The type of the entity. + public class ArtifactDeployState : ArtifactDeployState + where TArtifact : IArtifact + { + /// + /// Initializes a new instance of the class. + /// + public ArtifactDeployState() + { } + + /// + /// Initializes a new instance of the class. + /// + /// The artifact. + /// The entity. + /// The service connector deploying the artifact. + /// The next pass number. + public ArtifactDeployState(TArtifact art, TEntity entity, IServiceConnector connector, int nextPass) + { + Artifact = art; + Entity = entity; + Connector = connector; + NextPass = nextPass; + } + + /// + /// Gets or sets the artifact. + /// + public new TArtifact Artifact { get; set; } + + /// + /// Gets or sets the entity. + /// + public TEntity Entity { get; set; } + + /// + protected sealed override IArtifact GetArtifactAsIArtifact() + { + return Artifact; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/ArtifactSignature.cs b/src/Umbraco.Core/Deploy/ArtifactSignature.cs new file mode 100644 index 0000000000..4bea6a5ce8 --- /dev/null +++ b/src/Umbraco.Core/Deploy/ArtifactSignature.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Deploy +{ + public sealed class ArtifactSignature : IArtifactSignature + { + public ArtifactSignature(Udi udi, string checksum, IEnumerable dependencies = null) + { + Udi = udi; + Checksum = checksum; + Dependencies = dependencies ?? Enumerable.Empty(); + } + + public Udi Udi { get; private set; } + + public string Checksum { get; private set; } + + public IEnumerable Dependencies { get; private set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/Difference.cs b/src/Umbraco.Core/Deploy/Difference.cs new file mode 100644 index 0000000000..90329afdd6 --- /dev/null +++ b/src/Umbraco.Core/Deploy/Difference.cs @@ -0,0 +1,28 @@ +namespace Umbraco.Core.Deploy +{ + public class Difference + { + public Difference(string title, string text = null, string category = null) + { + Title = title; + Text = text; + Category = category; + } + + public string Title { get; set; } + public string Text { get; set; } + public string Category { get; set; } + + public override string ToString() + { + var s = Title; + if (!string.IsNullOrWhiteSpace(Category)) s += string.Format("[{0}]", Category); + if (!string.IsNullOrWhiteSpace(Text)) + { + if (s.Length > 0) s += ":"; + s += Text; + } + return s; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/Direction.cs b/src/Umbraco.Core/Deploy/Direction.cs new file mode 100644 index 0000000000..b0e1c1dc0a --- /dev/null +++ b/src/Umbraco.Core/Deploy/Direction.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Core.Deploy +{ + public enum Direction + { + ToArtifact, + FromArtifact + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IArtifact.cs b/src/Umbraco.Core/Deploy/IArtifact.cs new file mode 100644 index 0000000000..653e6386bb --- /dev/null +++ b/src/Umbraco.Core/Deploy/IArtifact.cs @@ -0,0 +1,11 @@ +namespace Umbraco.Core.Deploy +{ + /// + /// Represents an artifact ie an object that can be transfered between environments. + /// + public interface IArtifact : IArtifactSignature + { + string Name { get; } + string Alias { get; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IArtifactSignature.cs b/src/Umbraco.Core/Deploy/IArtifactSignature.cs new file mode 100644 index 0000000000..83b112586b --- /dev/null +++ b/src/Umbraco.Core/Deploy/IArtifactSignature.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Deploy +{ + /// + /// Represents the signature of an artifact. + /// + public interface IArtifactSignature + { + /// + /// Gets the entity unique identifier of this artifact. + /// + /// + /// The project identifier is independent from the state of the artifact, its data + /// values, dependencies, anything. It never changes and fully identifies the artifact. + /// What an entity uses as a unique identifier will influence what we can transfer + /// between environments. Eg content type "Foo" on one environment is not necessarily the + /// same as "Foo" on another environment, if guids are used as unique identifiers. What is + /// used should be documented for each entity, along with the consequences of the choice. + /// + Udi Udi { get; } + + /// + /// Gets the checksum of this artifact. + /// + /// + /// The checksum depends on the artifact's properties, and on the identifiers of all its dependencies, + /// but not on their checksums. So the checksum changes when any of the artifact's properties changes, + /// or when the list of dependencies changes. But not if one of these dependencies change. + /// It is assumed that checksum collisions cannot happen ie that no two different artifact's + /// states will ever produce the same checksum, so that if two artifacts have the same checksum then + /// they are identical. + /// + string Checksum { get; } + + /// + /// Gets the dependencies of this artifact. + /// + IEnumerable Dependencies { get; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IDeployContext.cs b/src/Umbraco.Core/Deploy/IDeployContext.cs new file mode 100644 index 0000000000..7d4066e015 --- /dev/null +++ b/src/Umbraco.Core/Deploy/IDeployContext.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Core.Deploy +{ + /// + /// Represents a deployment context. + /// + public interface IDeployContext + { + /// + /// Gets the unique identifier of the deployment. + /// + Guid SessionId { get; } + + /// + /// Gets the file source. + /// + /// The file source is used to obtain files from the source environment. + IFileSource FileSource { get; } + + /// + /// Gets the next number in a numerical sequence. + /// + /// The next sequence number. + /// Can be used to uniquely number things during a deployment. + int NextSeq(); + + /// + /// Gets items. + /// + IDictionary Items { get; } + + /// + /// Gets item. + /// + /// The type of the item. + /// The key of the item. + /// The item with the specified key and type, if any, else null. + T Item(string key) where T : class; + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IFileSource.cs b/src/Umbraco.Core/Deploy/IFileSource.cs new file mode 100644 index 0000000000..7d13e0fbde --- /dev/null +++ b/src/Umbraco.Core/Deploy/IFileSource.cs @@ -0,0 +1,91 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Umbraco.Core.Deploy +{ + /// + /// Represents a file source, ie a mean for a target environment involved in a + /// deployment to obtain the content of files being deployed. + /// + public interface IFileSource + { + /// + /// Gets the content of a file as a stream. + /// + /// A file entity identifier. + /// A stream with read access to the file content. + /// + /// Returns null if no content could be read. + /// The caller should ensure that the stream is properly closed/disposed. + /// + Stream GetFileStream(StringUdi udi); + + /// + /// Gets the content of a file as a stream. + /// + /// A file entity identifier. + /// A cancellation token. + /// A stream with read access to the file content. + /// + /// Returns null if no content could be read. + /// The caller should ensure that the stream is properly closed/disposed. + /// + Task GetFileStreamAsync(StringUdi udi, CancellationToken token); + + /// + /// Gets the content of a file as a string. + /// + /// A file entity identifier. + /// A string containing the file content. + /// Returns null if no content could be read. + string GetFileContent(StringUdi udi); + + /// + /// Gets the content of a file as a string. + /// + /// A file entity identifier. + /// A cancellation token. + /// A string containing the file content. + /// Returns null if no content could be read. + Task GetFileContentAsync(StringUdi udi, CancellationToken token); + + /// + /// Gets the length of a file. + /// + /// A file entity identifier. + /// The length of the file, or -1 if the file does not exist. + long GetFileLength(StringUdi udi); + + /// + /// Gets the length of a file. + /// + /// A file entity identifier. + /// A cancellation token. + /// The length of the file, or -1 if the file does not exist. + Task GetFileLengthAsync(StringUdi udi, CancellationToken token); + + /// + /// Gets files and store them using a file store. + /// + /// The udis of the files to get. + /// A collection of file types which can store the files. + void GetFiles(IEnumerable udis, IFileTypeCollection fileTypes); + + /// + /// Gets files and store them using a file store. + /// + /// The udis of the files to get. + /// A collection of file types which can store the files. + /// A cancellation token. + Task GetFilesAsync(IEnumerable udis, IFileTypeCollection fileTypes, CancellationToken token); + + ///// + ///// Gets the content of a file as a bytes array. + ///// + ///// A file entity identifier. + ///// A byte array containing the file content. + //byte[] GetFileBytes(StringUdi Udi); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IFileType.cs b/src/Umbraco.Core/Deploy/IFileType.cs new file mode 100644 index 0000000000..f7ab22ffae --- /dev/null +++ b/src/Umbraco.Core/Deploy/IFileType.cs @@ -0,0 +1,32 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Umbraco.Core.Deploy +{ + public interface IFileType + { + Stream GetStream(StringUdi udi); + + Task GetStreamAsync(StringUdi udi, CancellationToken token); + + Stream GetChecksumStream(StringUdi udi); + + long GetLength(StringUdi udi); + + void SetStream(StringUdi udi, Stream stream); + + Task SetStreamAsync(StringUdi udi, Stream stream, CancellationToken token); + + bool CanSetPhysical { get; } + + void Set(StringUdi udi, string physicalPath, bool copy = false); + + // this is not pretty as *everywhere* in Deploy we take care of ignoring + // the physical path and always rely on Core's virtual IFileSystem but + // Cloud wants to add some of these files to Git and needs the path... + string GetPhysicalPath(StringUdi udi); + + string GetVirtualPath(StringUdi udi); + } +} diff --git a/src/Umbraco.Core/Deploy/IFileTypeCollection.cs b/src/Umbraco.Core/Deploy/IFileTypeCollection.cs new file mode 100644 index 0000000000..9e2ab137d1 --- /dev/null +++ b/src/Umbraco.Core/Deploy/IFileTypeCollection.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Core.Deploy +{ + public interface IFileTypeCollection + { + IFileType this[string entityType] { get; } + + bool Contains(string entityType); + } +} diff --git a/src/Umbraco.Core/Deploy/IGridCellValueConnector.cs b/src/Umbraco.Core/Deploy/IGridCellValueConnector.cs new file mode 100644 index 0000000000..a4c5e7aeab --- /dev/null +++ b/src/Umbraco.Core/Deploy/IGridCellValueConnector.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using Umbraco.Core.Models; + +namespace Umbraco.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 + { + /// + /// 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); + + /// + /// Gets the value to be deployed from the control value as a string. + /// + /// The control containing the value. + /// The property where the control is located. Do not modify - only used for context + /// The dependencies of the property. + /// The grid cell value to be deployed. + /// Note that + string GetValue(GridValue.GridControl gridControl, Property property, ICollection dependencies); + + /// + /// Allows you to modify the value of a control being deployed. + /// + /// The control being deployed. + /// The property where the is located. Do not modify - only used for context. + /// Follows the pattern of the property value connectors (). The SetValue method is used to modify the value of the . + /// Note that only the value should be modified - not the . + /// The should only be used to assist with context data relevant when setting the value. + void SetValue(GridValue.GridControl gridControl, Property property); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IImageSourceParser.cs b/src/Umbraco.Core/Deploy/IImageSourceParser.cs new file mode 100644 index 0000000000..d8e8d860ac --- /dev/null +++ b/src/Umbraco.Core/Deploy/IImageSourceParser.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Deploy +{ + /// + /// Provides methods to parse image tag sources in property values. + /// + public interface IImageSourceParser + { + /// + /// Parses an Umbraco property value and produces an artifact property value. + /// + /// The property value. + /// A list of dependencies. + /// The parsed value. + /// Turns src="/media/..." into src="umb://media/..." and adds the corresponding udi to the dependencies. + string ToArtifact(string value, ICollection dependencies); + + /// + /// Parses an artifact property value and produces an Umbraco property value. + /// + /// The artifact property value. + /// The parsed value. + /// Turns umb://media/... into /media/.... + string FromArtifact(string value); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/ILocalLinkParser.cs b/src/Umbraco.Core/Deploy/ILocalLinkParser.cs new file mode 100644 index 0000000000..c5906c2060 --- /dev/null +++ b/src/Umbraco.Core/Deploy/ILocalLinkParser.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Deploy +{ + /// + /// Provides methods to parse local link tags in property values. + /// + public interface ILocalLinkParser + { + /// + /// Parses an Umbraco property value and produces an artifact property value. + /// + /// The property value. + /// A list of dependencies. + /// The parsed value. + /// Turns {{localLink:1234}} into {{localLink:umb://{type}/{id}}} and adds the corresponding udi to the dependencies. + string ToArtifact(string value, ICollection dependencies); + + /// + /// Parses an artifact property value and produces an Umbraco property value. + /// + /// The artifact property value. + /// The parsed value. + /// Turns {{localLink:umb://{type}/{id}}} into {{localLink:1234}}. + string FromArtifact(string value); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IMacroParser.cs b/src/Umbraco.Core/Deploy/IMacroParser.cs new file mode 100644 index 0000000000..9cde8ef8b6 --- /dev/null +++ b/src/Umbraco.Core/Deploy/IMacroParser.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Deploy +{ + public interface IMacroParser + { + /// + /// Parses an Umbraco property value and produces an artifact property value. + /// + /// Property value. + /// A list of dependencies. + /// Parsed value. + string ToArtifact(string value, ICollection dependencies); + + /// + /// Parses an artifact property value and produces an Umbraco property value. + /// + /// Artifact property value. + /// Parsed value. + string FromArtifact(string value); + + /// + /// Tries to replace the value of the attribute/parameter with a value containing a converted identifier. + /// + /// Value to attempt to convert + /// Alias of the editor used for the parameter + /// Collection to add dependencies to when performing ToArtifact + /// Indicates which action is being performed (to or from artifact) + /// Value with converted identifiers + string ReplaceAttributeValue(string value, string editorAlias, ICollection dependencies, Direction direction); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IPreValueConnector.cs b/src/Umbraco.Core/Deploy/IPreValueConnector.cs new file mode 100644 index 0000000000..4ef898cc74 --- /dev/null +++ b/src/Umbraco.Core/Deploy/IPreValueConnector.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Deploy +{ + /// + /// Defines methods that can convert a preValue to / from an environment-agnostic string. + /// + /// PreValues 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 IPreValueConnector + { + /// + /// Gets the property editor aliases that the value converter supports by default. + /// + IEnumerable PropertyEditorAliases { get; } + + /// + /// Gets the environment-agnostic preValues corresponding to environment-specific preValues. + /// + /// The environment-specific preValues. + /// The dependencies. + /// + IDictionary ConvertToDeploy(IDictionary preValues, ICollection dependencies); + + /// + /// Gets the environment-specific preValues corresponding to environment-agnostic preValues. + /// + /// The environment-agnostic preValues. + /// + IDictionary ConvertToLocalEnvironment(IDictionary preValues); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IServiceConnector.cs b/src/Umbraco.Core/Deploy/IServiceConnector.cs new file mode 100644 index 0000000000..b0b76dd854 --- /dev/null +++ b/src/Umbraco.Core/Deploy/IServiceConnector.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using Umbraco.Core.Plugins; + +namespace Umbraco.Core.Deploy +{ + /// + /// Connects to an Umbraco service. + /// + public interface IServiceConnector : IDiscoverable + { + /// + /// Gets an artifact. + /// + /// The entity identifier of the artifact. + /// The corresponding artifact, or null. + IArtifact GetArtifact(Udi udi); + + /// + /// Gets an artifact. + /// + /// The entity. + /// The corresponding artifact. + IArtifact GetArtifact(object entity); + + /// + /// Initializes processing for an artifact. + /// + /// The artifact. + /// The deploy context. + /// The mapped artifact. + ArtifactDeployState ProcessInit(IArtifact art, IDeployContext context); + + /// + /// Processes an artifact. + /// + /// The mapped artifact. + /// The deploy context. + /// The processing pass number. + void Process(ArtifactDeployState dart, IDeployContext context, int pass); + + /// + /// Explodes a range into udis. + /// + /// The range. + /// The list of udis where to add the new udis. + /// Also, it's cool to have a method named Explode. Kaboom! + void Explode(UdiRange range, List udis); + + /// + /// Gets a named range for a specified udi and selector. + /// + /// The udi. + /// The selector. + /// The named range for the specified udi and selector. + NamedUdiRange GetRange(Udi udi, string selector); + + /// + /// Gets a named range for specified entity type, identifier and selector. + /// + /// The entity type. + /// The identifier. + /// The selector. + /// The named range for the specified entity type, identifier and selector. + /// + /// This is temporary. At least we thought it would be, in sept. 2016. What day is it now? + /// At the moment our UI has a hard time returning proper udis, mainly because Core's tree do + /// not manage guids but only ints... so we have to provide a way to support it. The string id here + /// can be either a real string (for string udis) or an "integer as a string", using the value "-1" to + /// indicate the "root" i.e. an open udi. + /// + NamedUdiRange GetRange(string entityType, string sid, string selector); + + /// + /// Compares two artifacts. + /// + /// The first artifact. + /// The second artifact. + /// A collection of differences to append to, if not null. + /// A boolean value indicating whether the artifacts are identical. + /// ServiceConnectorBase{TArtifact} provides a very basic default implementation. + bool Compare(IArtifact art1, IArtifact art2, ICollection differences = null); + } + +} diff --git a/src/Umbraco.Core/Deploy/IValueConnector.cs b/src/Umbraco.Core/Deploy/IValueConnector.cs new file mode 100644 index 0000000000..a93e5a05a4 --- /dev/null +++ b/src/Umbraco.Core/Deploy/IValueConnector.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Deploy +{ + /// + /// Defines methods that can convert a property value to / from an environment-agnostic string. + /// + /// Property 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. Connectors also deal + /// with serializing to / from string. + public interface IValueConnector + { + /// + /// Gets the property editor aliases that the value converter supports by default. + /// + IEnumerable PropertyEditorAliases { get; } + + /// + /// Gets the deploy property corresponding to a content property. + /// + /// The content property. + /// The content dependencies. + /// The deploy property value. + string GetValue(Property property, ICollection dependencies); + + /// + /// Sets a content property value using a deploy property. + /// + /// The content item. + /// The property alias. + /// The deploy property value. + void SetValue(IContentBase content, string alias, string value); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Diagnostics/MiniDump.cs b/src/Umbraco.Core/Diagnostics/MiniDump.cs new file mode 100644 index 0000000000..e8c2e82f94 --- /dev/null +++ b/src/Umbraco.Core/Diagnostics/MiniDump.cs @@ -0,0 +1,135 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using Umbraco.Core.IO; + +namespace Umbraco.Core.Diagnostics +{ + // taken from https://blogs.msdn.microsoft.com/dondu/2010/10/24/writing-minidumps-in-c/ + // and https://blogs.msdn.microsoft.com/dondu/2010/10/31/writing-minidumps-from-exceptions-in-c/ + // which itself got it from http://blog.kalmbach-software.de/2008/12/13/writing-minidumps-in-c/ + + internal static class MiniDump + { + private static readonly object LockO = new object(); + + [Flags] + public enum Option : uint + { + // From dbghelp.h: + Normal = 0x00000000, + WithDataSegs = 0x00000001, + WithFullMemory = 0x00000002, + WithHandleData = 0x00000004, + FilterMemory = 0x00000008, + ScanMemory = 0x00000010, + WithUnloadedModules = 0x00000020, + WithIndirectlyReferencedMemory = 0x00000040, + FilterModulePaths = 0x00000080, + WithProcessThreadData = 0x00000100, + WithPrivateReadWriteMemory = 0x00000200, + WithoutOptionalData = 0x00000400, + WithFullMemoryInfo = 0x00000800, + WithThreadInfo = 0x00001000, + WithCodeSegs = 0x00002000, + WithoutAuxiliaryState = 0x00004000, + WithFullAuxiliaryState = 0x00008000, + WithPrivateWriteCopyMemory = 0x00010000, + IgnoreInaccessibleMemory = 0x00020000, + ValidTypeFlags = 0x0003ffff, + } + + //typedef struct _MINIDUMP_EXCEPTION_INFORMATION { + // DWORD ThreadId; + // PEXCEPTION_POINTERS ExceptionPointers; + // BOOL ClientPointers; + //} MINIDUMP_EXCEPTION_INFORMATION, *PMINIDUMP_EXCEPTION_INFORMATION; + [StructLayout(LayoutKind.Sequential, Pack = 4)] // Pack=4 is important! So it works also for x64! + public struct MiniDumpExceptionInformation + { + public uint ThreadId; + public IntPtr ExceptionPointers; + [MarshalAs(UnmanagedType.Bool)] + public bool ClientPointers; + } + + //BOOL + //WINAPI + //MiniDumpWriteDump( + // __in HANDLE hProcess, + // __in DWORD ProcessId, + // __in HANDLE hFile, + // __in MINIDUMP_TYPE DumpType, + // __in_opt PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, + // __in_opt PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, + // __in_opt PMINIDUMP_CALLBACK_INFORMATION CallbackParam + // ); + + // Overload requiring MiniDumpExceptionInformation + [DllImport("dbghelp.dll", EntryPoint = "MiniDumpWriteDump", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] + private static extern bool MiniDumpWriteDump(IntPtr hProcess, uint processId, SafeHandle hFile, uint dumpType, ref MiniDumpExceptionInformation expParam, IntPtr userStreamParam, IntPtr callbackParam); + + // Overload supporting MiniDumpExceptionInformation == NULL + [DllImport("dbghelp.dll", EntryPoint = "MiniDumpWriteDump", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] + private static extern bool MiniDumpWriteDump(IntPtr hProcess, uint processId, SafeHandle hFile, uint dumpType, IntPtr expParam, IntPtr userStreamParam, IntPtr callbackParam); + + [DllImport("kernel32.dll", EntryPoint = "GetCurrentThreadId", ExactSpelling = true)] + private static extern uint GetCurrentThreadId(); + + private static bool Write(SafeHandle fileHandle, Option options, bool withException = false) + { + var currentProcess = Process.GetCurrentProcess(); + var currentProcessHandle = currentProcess.Handle; + var currentProcessId = (uint)currentProcess.Id; + + MiniDumpExceptionInformation exp; + + exp.ThreadId = GetCurrentThreadId(); + exp.ClientPointers = false; + exp.ExceptionPointers = IntPtr.Zero; + + if (withException) + exp.ExceptionPointers = Marshal.GetExceptionPointers(); + + var bRet = exp.ExceptionPointers == IntPtr.Zero + ? MiniDumpWriteDump(currentProcessHandle, currentProcessId, fileHandle, (uint) options, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero) + : MiniDumpWriteDump(currentProcessHandle, currentProcessId, fileHandle, (uint) options, ref exp, IntPtr.Zero, IntPtr.Zero); + + return bRet; + } + + public static bool Dump(Option options = Option.WithFullMemory, bool withException = false) + { + lock (LockO) + { + // work around "stack trace is not available while minidump debugging", + // by making sure a local var (that we can inspect) contains the stack trace. + // getting the call stack before it is unwound would require a special exception + // filter everywhere in our code = not! + var stacktrace = withException ? Environment.StackTrace : string.Empty; + + var filepath = IOHelper.MapPath("~/App_Data/MiniDump"); + if (Directory.Exists(filepath) == false) + Directory.CreateDirectory(filepath); + + var filename = Path.Combine(filepath, string.Format("{0:yyyyMMddTHHmmss}.{1}.dmp", DateTime.UtcNow, Guid.NewGuid().ToString("N").Substring(0, 4))); + using (var stream = new FileStream(filename, FileMode.Create, FileAccess.ReadWrite, FileShare.Write)) + { + return Write(stream.SafeFileHandle, options, withException); + } + } + } + + public static bool OkToDump() + { + lock (LockO) + { + var filepath = IOHelper.MapPath("~/App_Data/MiniDump"); + if (Directory.Exists(filepath) == false) return true; + var count = Directory.GetFiles(filepath, "*.dmp").Length; + return count < 8; + } + } + } +} diff --git a/src/Umbraco.Core/EnumerableExtensions.cs b/src/Umbraco.Core/EnumerableExtensions.cs index f5679e4f7d..9b03a3a901 100644 --- a/src/Umbraco.Core/EnumerableExtensions.cs +++ b/src/Umbraco.Core/EnumerableExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; @@ -91,22 +92,15 @@ namespace Umbraco.Core } } - /// The for each. - /// The items. - /// The func. - /// item type - /// Result type - /// the Results + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use a normal foreach loop instead, this adds more allocations than necessary")] public static TResult[] ForEach(this IEnumerable items, Func func) { return items.Select(func).ToArray(); } - /// The for each. - /// The items. - /// The action. - /// Item type - /// list of TItem + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use a normal foreach loop instead, this adds more allocations than necessary")] public static IEnumerable ForEach(this IEnumerable items, Action action) { if (items != null) @@ -125,6 +119,7 @@ namespace Umbraco.Core /// The select child. /// Item type /// list of TItem + [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("Do not use, use SelectRecursive instead which has far less potential of re-iterating an iterator which may cause significantly more SQL queries")] public static IEnumerable FlattenList(this IEnumerable e, Func> f) { diff --git a/src/Umbraco.Core/Events/CancellableEventArgs.cs b/src/Umbraco.Core/Events/CancellableEventArgs.cs index 72cef19f7a..a102ea66ef 100644 --- a/src/Umbraco.Core/Events/CancellableEventArgs.cs +++ b/src/Umbraco.Core/Events/CancellableEventArgs.cs @@ -10,8 +10,8 @@ namespace Umbraco.Core.Events /// Event args for that can support cancellation /// [HostProtection(SecurityAction.LinkDemand, SharedState = true)] - public class CancellableEventArgs : EventArgs - { + public class CancellableEventArgs : EventArgs, IEquatable + { private bool _cancel; public CancellableEventArgs(bool canCancel, EventMessages messages, IDictionary additionalData) @@ -98,6 +98,36 @@ namespace Umbraco.Core.Events /// This allows for a bit of flexibility in our event raising - it's not pretty but we need to maintain backwards compatibility /// so we cannot change the strongly typed nature for some events. /// - public ReadOnlyDictionary AdditionalData { get; private set; } - } + public ReadOnlyDictionary AdditionalData { get; private set; } + + public bool Equals(CancellableEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(AdditionalData, other.AdditionalData); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((CancellableEventArgs) obj); + } + + public override int GetHashCode() + { + return (AdditionalData != null ? AdditionalData.GetHashCode() : 0); + } + + public static bool operator ==(CancellableEventArgs left, CancellableEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(CancellableEventArgs left, CancellableEventArgs right) + { + return !Equals(left, right); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs b/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs index a05f09ece5..7815747f18 100644 --- a/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs +++ b/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Security.Permissions; using Umbraco.Core.Models; @@ -10,7 +11,7 @@ namespace Umbraco.Core.Events /// /// [HostProtection(SecurityAction.LinkDemand, SharedState = true)] - public class CancellableObjectEventArgs : CancellableEventArgs + public class CancellableObjectEventArgs : CancellableEventArgs, IEquatable> { public CancellableObjectEventArgs(T eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) : base(canCancel, messages, additionalData) @@ -48,5 +49,37 @@ namespace Umbraco.Core.Events /// protected T EventObject { get; set; } + public bool Equals(CancellableObjectEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && EqualityComparer.Default.Equals(EventObject, other.EventObject); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((CancellableObjectEventArgs) obj); + } + + public override int GetHashCode() + { + unchecked + { + return (base.GetHashCode() * 397) ^ EqualityComparer.Default.GetHashCode(EventObject); + } + } + + public static bool operator ==(CancellableObjectEventArgs left, CancellableObjectEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(CancellableObjectEventArgs left, CancellableObjectEventArgs right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/CopyEventArgs.cs b/src/Umbraco.Core/Events/CopyEventArgs.cs index 16f4fae982..65dc54cc88 100644 --- a/src/Umbraco.Core/Events/CopyEventArgs.cs +++ b/src/Umbraco.Core/Events/CopyEventArgs.cs @@ -1,6 +1,9 @@ +using System; +using System.Collections.Generic; + namespace Umbraco.Core.Events { - public class CopyEventArgs : CancellableObjectEventArgs + public class CopyEventArgs : CancellableObjectEventArgs, IEquatable> { public CopyEventArgs(TEntity original, TEntity copy, bool canCancel, int parentId) : base(original, canCancel) @@ -43,5 +46,42 @@ namespace Umbraco.Core.Events public int ParentId { get; private set; } public bool RelateToOriginal { get; set; } + + public bool Equals(CopyEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && EqualityComparer.Default.Equals(Copy, other.Copy) && ParentId == other.ParentId && RelateToOriginal == other.RelateToOriginal; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((CopyEventArgs) obj); + } + + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ EqualityComparer.Default.GetHashCode(Copy); + hashCode = (hashCode * 397) ^ ParentId; + hashCode = (hashCode * 397) ^ RelateToOriginal.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(CopyEventArgs left, CopyEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(CopyEventArgs left, CopyEventArgs right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/DeleteEventArgs.cs b/src/Umbraco.Core/Events/DeleteEventArgs.cs index 1025066bcc..df13363b95 100644 --- a/src/Umbraco.Core/Events/DeleteEventArgs.cs +++ b/src/Umbraco.Core/Events/DeleteEventArgs.cs @@ -1,8 +1,9 @@ +using System; using System.Collections.Generic; namespace Umbraco.Core.Events { - public class DeleteEventArgs : CancellableObjectEventArgs> + public class DeleteEventArgs : CancellableObjectEventArgs>, IEquatable>, IDeletingMediaFilesEventArgs { /// /// Constructor accepting multiple entities that are used in the delete operation @@ -99,10 +100,43 @@ namespace Umbraco.Core.Events /// /// A list of media files that can be added to during a deleted operation for which Umbraco will ensure are removed /// - public List MediaFilesToDelete { get; private set; } + public List MediaFilesToDelete { get; private set; } + + public bool Equals(DeleteEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && MediaFilesToDelete.Equals(other.MediaFilesToDelete); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((DeleteEventArgs) obj); + } + + public override int GetHashCode() + { + unchecked + { + return (base.GetHashCode() * 397) ^ MediaFilesToDelete.GetHashCode(); + } + } + + public static bool operator ==(DeleteEventArgs left, DeleteEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(DeleteEventArgs left, DeleteEventArgs right) + { + return !Equals(left, right); + } } - public class DeleteEventArgs : CancellableEventArgs + public class DeleteEventArgs : CancellableEventArgs, IEquatable { public DeleteEventArgs(int id, bool canCancel, EventMessages eventMessages) : base(canCancel, eventMessages) @@ -125,5 +159,38 @@ namespace Umbraco.Core.Events /// Gets the Id of the object being deleted. /// public int Id { get; private set; } + + public bool Equals(DeleteEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && Id == other.Id; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((DeleteEventArgs) obj); + } + + public override int GetHashCode() + { + unchecked + { + return (base.GetHashCode() * 397) ^ Id; + } + } + + public static bool operator ==(DeleteEventArgs left, DeleteEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(DeleteEventArgs left, DeleteEventArgs right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/DeleteRevisionsEventArgs.cs b/src/Umbraco.Core/Events/DeleteRevisionsEventArgs.cs index 4d53270608..1db1296640 100644 --- a/src/Umbraco.Core/Events/DeleteRevisionsEventArgs.cs +++ b/src/Umbraco.Core/Events/DeleteRevisionsEventArgs.cs @@ -2,7 +2,7 @@ using System; namespace Umbraco.Core.Events { - public class DeleteRevisionsEventArgs : DeleteEventArgs + public class DeleteRevisionsEventArgs : DeleteEventArgs, IEquatable { public DeleteRevisionsEventArgs(int id, bool canCancel, Guid specificVersion = default(Guid), bool deletePriorVersions = false, DateTime dateToRetain = default(DateTime)) : base(id, canCancel) @@ -31,5 +31,42 @@ namespace Umbraco.Core.Events { get { return SpecificVersion != default(Guid); } } + + public bool Equals(DeleteRevisionsEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && DateToRetain.Equals(other.DateToRetain) && DeletePriorVersions == other.DeletePriorVersions && SpecificVersion.Equals(other.SpecificVersion); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((DeleteRevisionsEventArgs) obj); + } + + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ DateToRetain.GetHashCode(); + hashCode = (hashCode * 397) ^ DeletePriorVersions.GetHashCode(); + hashCode = (hashCode * 397) ^ SpecificVersion.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(DeleteRevisionsEventArgs left, DeleteRevisionsEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(DeleteRevisionsEventArgs left, DeleteRevisionsEventArgs right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/EventDefinition.cs b/src/Umbraco.Core/Events/EventDefinition.cs new file mode 100644 index 0000000000..e861e9496e --- /dev/null +++ b/src/Umbraco.Core/Events/EventDefinition.cs @@ -0,0 +1,73 @@ +using System; + +namespace Umbraco.Core.Events +{ + internal class EventDefinition : EventDefinitionBase + { + private readonly EventHandler _trackedEvent; + private readonly object _sender; + private readonly EventArgs _args; + + public EventDefinition(EventHandler trackedEvent, object sender, EventArgs args, string eventName = null) + : base(sender, args, eventName) + { + _trackedEvent = trackedEvent; + _sender = sender; + _args = args; + } + + public override void RaiseEvent() + { + if (_trackedEvent != null) + { + _trackedEvent(_sender, _args); + } + } + } + + internal class EventDefinition : EventDefinitionBase + { + private readonly EventHandler _trackedEvent; + private readonly object _sender; + private readonly TEventArgs _args; + + public EventDefinition(EventHandler trackedEvent, object sender, TEventArgs args, string eventName = null) + : base(sender, args, eventName) + { + _trackedEvent = trackedEvent; + _sender = sender; + _args = args; + } + + public override void RaiseEvent() + { + if (_trackedEvent != null) + { + _trackedEvent(_sender, _args); + } + } + } + + internal class EventDefinition : EventDefinitionBase + { + private readonly TypedEventHandler _trackedEvent; + private readonly TSender _sender; + private readonly TEventArgs _args; + + public EventDefinition(TypedEventHandler trackedEvent, TSender sender, TEventArgs args, string eventName = null) + : base(sender, args, eventName) + { + _trackedEvent = trackedEvent; + _sender = sender; + _args = args; + } + + public override void RaiseEvent() + { + if (_trackedEvent != null) + { + _trackedEvent(_sender, _args); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/EventDefinitionBase.cs b/src/Umbraco.Core/Events/EventDefinitionBase.cs new file mode 100644 index 0000000000..c0a061f80f --- /dev/null +++ b/src/Umbraco.Core/Events/EventDefinitionBase.cs @@ -0,0 +1,70 @@ +using System; +using System.Reflection; + +namespace Umbraco.Core.Events +{ + public abstract class EventDefinitionBase : IEventDefinition, IEquatable + { + protected EventDefinitionBase(object sender, object args, string eventName = null) + { + if (sender == null) throw new ArgumentNullException("sender"); + if (args == null) throw new ArgumentNullException("args"); + Sender = sender; + Args = args; + EventName = eventName; + + if (EventName.IsNullOrWhiteSpace()) + { + var findResult = EventNameExtractor.FindEvent(sender, args, + //don't match "Ing" suffixed names + exclude:EventNameExtractor.MatchIngNames); + + if (findResult.Success == false) + throw new AmbiguousMatchException("Could not automatically find the event name, the event name will need to be explicitly registered for this event definition. Error: " + findResult.Result.Error); + EventName = findResult.Result.Name; + } + } + + public object Sender { get; private set; } + public object Args { get; private set; } + public string EventName { get; private set; } + + public abstract void RaiseEvent(); + + public bool Equals(EventDefinitionBase other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Args.Equals(other.Args) && string.Equals(EventName, other.EventName) && Sender.Equals(other.Sender); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((EventDefinitionBase) obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = Args.GetHashCode(); + hashCode = (hashCode * 397) ^ EventName.GetHashCode(); + hashCode = (hashCode * 397) ^ Sender.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(EventDefinitionBase left, EventDefinitionBase right) + { + return Equals(left, right); + } + + public static bool operator !=(EventDefinitionBase left, EventDefinitionBase right) + { + return Equals(left, right) == false; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/EventDefinitionFilter.cs b/src/Umbraco.Core/Events/EventDefinitionFilter.cs new file mode 100644 index 0000000000..4bbe75d10b --- /dev/null +++ b/src/Umbraco.Core/Events/EventDefinitionFilter.cs @@ -0,0 +1,24 @@ +namespace Umbraco.Core.Events +{ + /// + /// The filter used in the GetEvents method which determines + /// how the result list is filtered + /// + public enum EventDefinitionFilter + { + /// + /// Returns all events tracked + /// + All, + + /// + /// Deduplicates events and only returns the first duplicate instance tracked + /// + FirstIn, + + /// + /// Deduplicates events and only returns the last duplicate instance tracked + /// + LastIn + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/EventExtensions.cs b/src/Umbraco.Core/Events/EventExtensions.cs index bd66b605fd..a366ad0cce 100644 --- a/src/Umbraco.Core/Events/EventExtensions.cs +++ b/src/Umbraco.Core/Events/EventExtensions.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; +using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Events { @@ -10,73 +12,40 @@ namespace Umbraco.Core.Events /// public static class EventExtensions { - /// - /// Raises the event and returns a boolean value indicating if the event was cancelled - /// - /// - /// - /// - /// - /// - /// - public static bool IsRaisedEventCancelled( - this TypedEventHandler eventHandler, - TArgs args, - TSender sender) - where TArgs : CancellableEventArgs - { - if (eventHandler != null) - eventHandler(sender, args); + // keep these two for backward compatibility reasons but understand that + // they are *not* part of any scope / event dispatcher / anything... + /// + /// Raises a cancelable event and returns a value indicating whether the event should be cancelled. + /// + /// The type of the event source. + /// The type of the event data. + /// The event handler. + /// The event source. + /// The event data. + /// A value indicating whether the cancelable event should be cancelled + /// A cancelable event is raised by a component when it is about to perform an action that can be canceled. + public static bool IsRaisedEventCancelled(this TypedEventHandler eventHandler, TArgs args, TSender sender) + where TArgs : CancellableEventArgs + { + if (eventHandler == null) return args.Cancel; + eventHandler(sender, args); return args.Cancel; } - + /// - /// Raises the event + /// Raises an event. /// - /// - /// - /// - /// - /// - public static void RaiseEvent( - this TypedEventHandler eventHandler, - TArgs args, - TSender sender) - where TArgs : EventArgs - { - if (eventHandler != null) - eventHandler(sender, args); - } - - // moves the last handler that was added to an instance event, to first position - public static void PromoteLastHandler(object sender, string eventName) - { - var fieldInfo = sender.GetType().GetField(eventName, BindingFlags.Instance | BindingFlags.NonPublic); - if (fieldInfo == null) throw new InvalidOperationException("No event named " + eventName + "."); - PromoteLastHandler(sender, fieldInfo); - } - - // moves the last handler that was added to a static event, to first position - public static void PromoteLastHandler(string eventName) + /// The type of the event source. + /// The type of the event data. + /// The event handler. + /// The event source. + /// The event data. + public static void RaiseEvent(this TypedEventHandler eventHandler, TArgs args, TSender sender) + where TArgs : EventArgs { - var fieldInfo = typeof(TSender).GetField(eventName, BindingFlags.Static | BindingFlags.NonPublic); - if (fieldInfo == null) throw new InvalidOperationException("No event named " + eventName + "."); - PromoteLastHandler(null, fieldInfo); - } - - private static void PromoteLastHandler(object sender, FieldInfo fieldInfo) - { - var d = fieldInfo.GetValue(sender) as Delegate; - if (d == null) return; - - var l = d.GetInvocationList(); - var x = l[l.Length - 1]; - for (var i = l.Length - 1; i > 0; i--) - l[i] = l[i - 1]; - l[0] = x; - - fieldInfo.SetValue(sender, Delegate.Combine(l)); + if (eventHandler == null) return; + eventHandler(sender, args); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/MessageType.cs b/src/Umbraco.Core/Events/EventMessageType.cs similarity index 100% rename from src/Umbraco.Core/Events/MessageType.cs rename to src/Umbraco.Core/Events/EventMessageType.cs diff --git a/src/Umbraco.Core/Events/EventNameExtractor.cs b/src/Umbraco.Core/Events/EventNameExtractor.cs new file mode 100644 index 0000000000..33137770d4 --- /dev/null +++ b/src/Umbraco.Core/Events/EventNameExtractor.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace Umbraco.Core.Events +{ + /// + /// There is actually no way to discover an event name in c# at the time of raising the event. It is possible + /// to get the event name from the handler that is being executed based on the event being raised, however that is not + /// what we want in this case. We need to find the event name before it is being raised - you would think that it's possible + /// with reflection or anything but that is not the case, the delegate that defines an event has no info attached to it, it + /// is literally just an event. + /// + /// So what this does is take the sender and event args objects, looks up all public/static events on the sender that have + /// a generic event handler with generic arguments (but only) one, then we match the type of event arguments with the ones + /// being passed in. As it turns out, in our services this will work for the majority of our events! In some cases it may not + /// work and we'll have to supply a string but hopefully this saves a bit of magic strings. + /// + /// We can also write tests to validate these are all working correctly for all services. + /// + internal class EventNameExtractor + { + + /// + /// Finds the event name on the sender that matches the args type + /// + /// + /// + /// + /// A filter to exclude matched event names, this filter should return true to exclude the event name from being matched + /// + /// + /// null if not found or an ambiguous match + /// + public static Attempt FindEvent(Type senderType, Type argsType, Func exclude) + { + var found = MatchedEventNames.GetOrAdd(new Tuple(senderType, argsType), tuple => + { + var events = CandidateEvents.GetOrAdd(senderType, t => + { + return t.GetEvents(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) + //we can only look for events handlers with generic types because that is the only + // way that we can try to find a matching event based on the arg type passed in + .Where(x => x.EventHandlerType.IsGenericType) + .Select(x => new EventInfoArgs(x, x.EventHandlerType.GetGenericArguments())) + //we are only looking for event handlers that have more than one generic argument + .Where(x => + { + if (x.GenericArgs.Length == 1) return true; + + //special case for our own TypedEventHandler + if (x.EventInfo.EventHandlerType.GetGenericTypeDefinition() == typeof(TypedEventHandler<,>) && x.GenericArgs.Length == 2) + { + return true; + } + + return false; + }) + .ToArray(); + }); + + return events.Where(x => + { + if (x.GenericArgs.Length == 1 && x.GenericArgs[0] == tuple.Item2) + return true; + + //special case for our own TypedEventHandler + if (x.EventInfo.EventHandlerType.GetGenericTypeDefinition() == typeof(TypedEventHandler<,>) + && x.GenericArgs.Length == 2 + && x.GenericArgs[1] == tuple.Item2) + { + return true; + } + + return false; + }).Select(x => x.EventInfo.Name).ToArray(); + }); + + var filtered = found.Where(x => exclude(x) == false).ToArray(); + + if (filtered.Length == 0) + return Attempt.Fail(new EventNameExtractorResult(EventNameExtractorError.NoneFound)); + + if (filtered.Length == 1) + return Attempt.Succeed(new EventNameExtractorResult(filtered[0])); + + //there's more than one left so it's ambiguous! + return Attempt.Fail(new EventNameExtractorResult(EventNameExtractorError.Ambiguous)); + } + + /// + /// Finds the event name on the sender that matches the args type + /// + /// + /// + /// + /// A filter to exclude matched event names, this filter should return true to exclude the event name from being matched + /// + /// + /// null if not found or an ambiguous match + /// + public static Attempt FindEvent(object sender, object args, Func exclude) + { + return FindEvent(sender.GetType(), args.GetType(), exclude); + } + + /// + /// Return true if the event is named with an ING name such as "Saving" or "RollingBack" + /// + /// + /// + internal static bool MatchIngNames(string eventName) + { + var splitter = new Regex(@"(? + /// Return true if the event is not named with an ING name such as "Saving" or "RollingBack" + /// + /// + /// + internal static bool MatchNonIngNames(string eventName) + { + var splitter = new Regex(@"(? + /// Used to cache all candidate events for a given type so we don't re-look them up + /// + private static readonly ConcurrentDictionary CandidateEvents = new ConcurrentDictionary(); + + /// + /// Used to cache all matched event names by (sender type + arg type) so we don't re-look them up + /// + private static readonly ConcurrentDictionary, string[]> MatchedEventNames = new ConcurrentDictionary, string[]>(); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/EventNameExtractorError.cs b/src/Umbraco.Core/Events/EventNameExtractorError.cs new file mode 100644 index 0000000000..3b27db2a1a --- /dev/null +++ b/src/Umbraco.Core/Events/EventNameExtractorError.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Core.Events +{ + internal enum EventNameExtractorError + { + NoneFound, + Ambiguous + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/EventNameExtractorResult.cs b/src/Umbraco.Core/Events/EventNameExtractorResult.cs new file mode 100644 index 0000000000..6213882d1f --- /dev/null +++ b/src/Umbraco.Core/Events/EventNameExtractorResult.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Core.Events +{ + internal class EventNameExtractorResult + { + public EventNameExtractorError? Error { get; private set; } + public string Name { get; private set; } + + public EventNameExtractorResult(string name) + { + Name = name; + } + + public EventNameExtractorResult(EventNameExtractorError error) + { + Error = error; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/ExportEventArgs.cs b/src/Umbraco.Core/Events/ExportEventArgs.cs index 161a073615..acbf920636 100644 --- a/src/Umbraco.Core/Events/ExportEventArgs.cs +++ b/src/Umbraco.Core/Events/ExportEventArgs.cs @@ -1,9 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Xml.Linq; namespace Umbraco.Core.Events { - public class ExportEventArgs : CancellableObjectEventArgs> + public class ExportEventArgs : CancellableObjectEventArgs>, IEquatable> { /// /// Constructor accepting a single entity instance @@ -48,5 +49,38 @@ namespace Umbraco.Core.Events /// Returns the xml relating to the export event /// public XElement Xml { get; private set; } + + public bool Equals(ExportEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && Equals(Xml, other.Xml); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((ExportEventArgs) obj); + } + + public override int GetHashCode() + { + unchecked + { + return (base.GetHashCode() * 397) ^ (Xml != null ? Xml.GetHashCode() : 0); + } + } + + public static bool operator ==(ExportEventArgs left, ExportEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(ExportEventArgs left, ExportEventArgs right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/IDeletingMediaFilesEventArgs.cs b/src/Umbraco.Core/Events/IDeletingMediaFilesEventArgs.cs new file mode 100644 index 0000000000..45681042ba --- /dev/null +++ b/src/Umbraco.Core/Events/IDeletingMediaFilesEventArgs.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Events +{ + internal interface IDeletingMediaFilesEventArgs + { + List MediaFilesToDelete { get; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/IEventDefinition.cs b/src/Umbraco.Core/Events/IEventDefinition.cs new file mode 100644 index 0000000000..eb4fe65eb8 --- /dev/null +++ b/src/Umbraco.Core/Events/IEventDefinition.cs @@ -0,0 +1,13 @@ +using System; + +namespace Umbraco.Core.Events +{ + public interface IEventDefinition + { + object Sender { get; } + object Args { get; } + string EventName { get; } + + void RaiseEvent(); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/IEventDispatcher.cs b/src/Umbraco.Core/Events/IEventDispatcher.cs new file mode 100644 index 0000000000..78f8ed3a4a --- /dev/null +++ b/src/Umbraco.Core/Events/IEventDispatcher.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Core.Events +{ + /// + /// Dispatches events from within a scope. + /// + /// + /// The name of the event is auto-magically discovered by matching the sender type, args type, and + /// eventHandler type. If the match is not unique, then the name parameter must be used to specify the + /// name in an explicit way. + /// What happens when an event is dispatched depends on the scope settings. It can be anything from + /// "trigger immediately" to "just ignore". Refer to the scope documentation for more details. + /// + public interface IEventDispatcher + { + // not sure about the Dispatch & DispatchCancelable signatures at all for now + // nor about the event name thing, etc - but let's keep it like this + + /// + /// Dispatches a cancelable event. + /// + /// The event handler. + /// The object that raised the event. + /// The event data. + /// The optional name of the event. + /// A value indicating whether the cancelable event was cancelled. + /// See general remarks on the interface. + bool DispatchCancelable(EventHandler eventHandler, object sender, CancellableEventArgs args, string name = null); + + /// + /// Dispatches a cancelable event. + /// + /// The event handler. + /// The object that raised the event. + /// The event data. + /// The optional name of the event. + /// A value indicating whether the cancelable event was cancelled. + /// See general remarks on the interface. + bool DispatchCancelable(EventHandler eventHandler, object sender, TArgs args, string name = null) + where TArgs : CancellableEventArgs; + + /// + /// Dispatches a cancelable event. + /// + /// The event handler. + /// The object that raised the event. + /// The event data. + /// The optional name of the event. + /// A value indicating whether the cancelable event was cancelled. + /// See general remarks on the interface. + bool DispatchCancelable(TypedEventHandler eventHandler, TSender sender, TArgs args, string name = null) + where TArgs : CancellableEventArgs; + + /// + /// Dispatches an event. + /// + /// The event handler. + /// The object that raised the event. + /// The event data. + /// The optional name of the event. + /// See general remarks on the interface. + void Dispatch(EventHandler eventHandler, object sender, EventArgs args, string name = null); + + /// + /// Dispatches an event. + /// + /// The event handler. + /// The object that raised the event. + /// The event data. + /// The optional name of the event. + /// See general remarks on the interface. + void Dispatch(EventHandler eventHandler, object sender, TArgs args, string name = null); + + /// + /// Dispatches an event. + /// + /// The event handler. + /// The object that raised the event. + /// The event data. + /// The optional name of the event. + /// See general remarks on the interface. + void Dispatch(TypedEventHandler eventHandler, TSender sender, TArgs args, string name = null); + + /// + /// Notifies the dispatcher that the scope is exiting. + /// + /// A value indicating whether the scope completed. + void ScopeExit(bool completed); + + /// + /// Gets the collected events. + /// + /// The collected events. + IEnumerable GetEvents(EventDefinitionFilter filter); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/ImportEventArgs.cs b/src/Umbraco.Core/Events/ImportEventArgs.cs index 3bdd6d6fcf..dcecf5c36b 100644 --- a/src/Umbraco.Core/Events/ImportEventArgs.cs +++ b/src/Umbraco.Core/Events/ImportEventArgs.cs @@ -1,9 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Xml.Linq; namespace Umbraco.Core.Events { - public class ImportEventArgs : CancellableObjectEventArgs> + public class ImportEventArgs : CancellableObjectEventArgs>, IEquatable> { /// /// Constructor accepting an XElement with the xml being imported @@ -46,5 +47,38 @@ namespace Umbraco.Core.Events /// Returns the xml relating to the import event /// public XElement Xml { get; private set; } + + public bool Equals(ImportEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && Equals(Xml, other.Xml); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((ImportEventArgs) obj); + } + + public override int GetHashCode() + { + unchecked + { + return (base.GetHashCode() * 397) ^ (Xml != null ? Xml.GetHashCode() : 0); + } + } + + public static bool operator ==(ImportEventArgs left, ImportEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(ImportEventArgs left, ImportEventArgs right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/ImportPackageEventArgs.cs b/src/Umbraco.Core/Events/ImportPackageEventArgs.cs index acc19ae968..06ce0c60e2 100644 --- a/src/Umbraco.Core/Events/ImportPackageEventArgs.cs +++ b/src/Umbraco.Core/Events/ImportPackageEventArgs.cs @@ -1,9 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core.Models.Packaging; namespace Umbraco.Core.Events { - internal class ImportPackageEventArgs : CancellableObjectEventArgs> + internal class ImportPackageEventArgs : CancellableObjectEventArgs>, IEquatable> { private readonly MetaData _packageMetaData; @@ -21,6 +22,44 @@ namespace Umbraco.Core.Events public MetaData PackageMetaData { get { return _packageMetaData; } + } + + public IEnumerable InstallationSummary + { + get { return EventObject; } + } + + public bool Equals(ImportPackageEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && _packageMetaData.Equals(other._packageMetaData); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((ImportPackageEventArgs) obj); + } + + public override int GetHashCode() + { + unchecked + { + return (base.GetHashCode() * 397) ^ _packageMetaData.GetHashCode(); + } + } + + public static bool operator ==(ImportPackageEventArgs left, ImportPackageEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(ImportPackageEventArgs left, ImportPackageEventArgs right) + { + return !Equals(left, right); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/MigrationEventArgs.cs b/src/Umbraco.Core/Events/MigrationEventArgs.cs index 5590ed4d26..61c56ecd8f 100644 --- a/src/Umbraco.Core/Events/MigrationEventArgs.cs +++ b/src/Umbraco.Core/Events/MigrationEventArgs.cs @@ -7,7 +7,7 @@ using Umbraco.Core.Persistence.Migrations; namespace Umbraco.Core.Events { - public class MigrationEventArgs : CancellableObjectEventArgs> + public class MigrationEventArgs : CancellableObjectEventArgs>, IEquatable { /// /// Constructor accepting multiple migrations that are used in the migration runner @@ -31,13 +31,13 @@ namespace Umbraco.Core.Events [Obsolete("Use constructor accepting a product name instead.")] [EditorBrowsable(EditorBrowsableState.Never)] public MigrationEventArgs(IList eventObject, SemVersion configuredVersion, SemVersion targetVersion, bool canCancel) - : this(eventObject, null, configuredVersion, targetVersion, GlobalSettings.UmbracoMigrationName, canCancel) + : this(eventObject, null, configuredVersion, targetVersion, Constants.System.UmbracoMigrationName, canCancel) { } [Obsolete("Use constructor accepting SemVersion instances and a product name instead.")] [EditorBrowsable(EditorBrowsableState.Never)] public MigrationEventArgs(IList eventObject, Version configuredVersion, Version targetVersion, bool canCancel) - : this(eventObject, null, new SemVersion(configuredVersion), new SemVersion(targetVersion), GlobalSettings.UmbracoMigrationName, canCancel) + : this(eventObject, null, new SemVersion(configuredVersion), new SemVersion(targetVersion), Constants.System.UmbracoMigrationName, canCancel) { } /// @@ -74,7 +74,7 @@ namespace Umbraco.Core.Events MigrationContext = migrationContext; ConfiguredSemVersion = configuredVersion; TargetSemVersion = targetVersion; - ProductName = GlobalSettings.UmbracoMigrationName; + ProductName = Constants.System.UmbracoMigrationName; } /// @@ -97,13 +97,13 @@ namespace Umbraco.Core.Events [Obsolete("Use constructor accepting a product name instead.")] [EditorBrowsable(EditorBrowsableState.Never)] public MigrationEventArgs(IList eventObject, SemVersion configuredVersion, SemVersion targetVersion) - : this(eventObject, null, configuredVersion, targetVersion, GlobalSettings.UmbracoMigrationName, false) + : this(eventObject, null, configuredVersion, targetVersion, Constants.System.UmbracoMigrationName, false) { } [Obsolete("Use constructor accepting SemVersion instances and a product name instead.")] [EditorBrowsable(EditorBrowsableState.Never)] public MigrationEventArgs(IList eventObject, Version configuredVersion, Version targetVersion) - : this(eventObject, null, new SemVersion(configuredVersion), new SemVersion(targetVersion), GlobalSettings.UmbracoMigrationName, false) + : this(eventObject, null, new SemVersion(configuredVersion), new SemVersion(targetVersion), Constants.System.UmbracoMigrationName, false) { } /// @@ -141,5 +141,43 @@ namespace Umbraco.Core.Events public string ProductName { get; private set; } internal IMigrationContext MigrationContext { get; private set; } + + public bool Equals(MigrationEventArgs 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) && string.Equals(ProductName, other.ProductName) && TargetSemVersion.Equals(other.TargetSemVersion); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((MigrationEventArgs) obj); + } + + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ ConfiguredSemVersion.GetHashCode(); + hashCode = (hashCode * 397) ^ MigrationContext.GetHashCode(); + hashCode = (hashCode * 397) ^ ProductName.GetHashCode(); + hashCode = (hashCode * 397) ^ TargetSemVersion.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(MigrationEventArgs left, MigrationEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(MigrationEventArgs left, MigrationEventArgs right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/MoveEventArgs.cs b/src/Umbraco.Core/Events/MoveEventArgs.cs index 0f0a5183a9..228e1ca2f7 100644 --- a/src/Umbraco.Core/Events/MoveEventArgs.cs +++ b/src/Umbraco.Core/Events/MoveEventArgs.cs @@ -4,7 +4,7 @@ using System.Linq; namespace Umbraco.Core.Events { - public class MoveEventArgs : CancellableObjectEventArgs + public class MoveEventArgs : CancellableObjectEventArgs, IEquatable> { /// /// Constructor accepting a collection of MoveEventInfo objects @@ -123,5 +123,38 @@ namespace Umbraco.Core.Events /// [Obsolete("Retrieve the ParentId from the MoveInfoCollection property instead")] public int ParentId { get; private set; } + + public bool Equals(MoveEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && MoveInfoCollection.Equals(other.MoveInfoCollection); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((MoveEventArgs) obj); + } + + public override int GetHashCode() + { + unchecked + { + return (base.GetHashCode() * 397) ^ MoveInfoCollection.GetHashCode(); + } + } + + public static bool operator ==(MoveEventArgs left, MoveEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(MoveEventArgs left, MoveEventArgs right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/MoveEventInfo.cs b/src/Umbraco.Core/Events/MoveEventInfo.cs index a74db7f36e..9e77971837 100644 --- a/src/Umbraco.Core/Events/MoveEventInfo.cs +++ b/src/Umbraco.Core/Events/MoveEventInfo.cs @@ -1,6 +1,9 @@ +using System; +using System.Collections.Generic; + namespace Umbraco.Core.Events { - public class MoveEventInfo + public class MoveEventInfo : IEquatable> { public MoveEventInfo(TEntity entity, string originalPath, int newParentId) { @@ -12,5 +15,41 @@ namespace Umbraco.Core.Events public TEntity Entity { get; set; } public string OriginalPath { get; set; } public int NewParentId { get; set; } + + public bool Equals(MoveEventInfo other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return EqualityComparer.Default.Equals(Entity, other.Entity) && NewParentId == other.NewParentId && string.Equals(OriginalPath, other.OriginalPath); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((MoveEventInfo) obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = EqualityComparer.Default.GetHashCode(Entity); + hashCode = (hashCode * 397) ^ NewParentId; + hashCode = (hashCode * 397) ^ OriginalPath.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(MoveEventInfo left, MoveEventInfo right) + { + return Equals(left, right); + } + + public static bool operator !=(MoveEventInfo left, MoveEventInfo right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/NewEventArgs.cs b/src/Umbraco.Core/Events/NewEventArgs.cs index acfd64e60d..415d82b90e 100644 --- a/src/Umbraco.Core/Events/NewEventArgs.cs +++ b/src/Umbraco.Core/Events/NewEventArgs.cs @@ -1,8 +1,10 @@ +using System; +using System.Collections.Generic; using Umbraco.Core.Models; namespace Umbraco.Core.Events { - public class NewEventArgs : CancellableObjectEventArgs + public class NewEventArgs : CancellableObjectEventArgs, IEquatable> { @@ -84,5 +86,42 @@ namespace Umbraco.Core.Events /// Gets or Sets the parent IContent object. /// public TEntity Parent { get; private set; } + + public bool Equals(NewEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && string.Equals(Alias, other.Alias) && EqualityComparer.Default.Equals(Parent, other.Parent) && ParentId == other.ParentId; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((NewEventArgs) obj); + } + + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ Alias.GetHashCode(); + hashCode = (hashCode * 397) ^ EqualityComparer.Default.GetHashCode(Parent); + hashCode = (hashCode * 397) ^ ParentId; + return hashCode; + } + } + + public static bool operator ==(NewEventArgs left, NewEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(NewEventArgs left, NewEventArgs right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/PassThroughEventDispatcher.cs b/src/Umbraco.Core/Events/PassThroughEventDispatcher.cs new file mode 100644 index 0000000000..4916fd13fd --- /dev/null +++ b/src/Umbraco.Core/Events/PassThroughEventDispatcher.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Events +{ + /// + /// This event manager supports event cancellation and will raise the events as soon as they are tracked, it does not store tracked events + /// + internal class PassThroughEventDispatcher : IEventDispatcher + { + public bool DispatchCancelable(EventHandler eventHandler, object sender, CancellableEventArgs args, string eventName = null) + { + if (eventHandler == null) return args.Cancel; + eventHandler(sender, args); + return args.Cancel; + } + + public bool DispatchCancelable(EventHandler eventHandler, object sender, TArgs args, string eventName = null) + where TArgs : CancellableEventArgs + { + if (eventHandler == null) return args.Cancel; + eventHandler(sender, args); + return args.Cancel; + } + + public bool DispatchCancelable(TypedEventHandler eventHandler, TSender sender, TArgs args, string eventName = null) + where TArgs : CancellableEventArgs + { + if (eventHandler == null) return args.Cancel; + eventHandler(sender, args); + return args.Cancel; + } + + public void Dispatch(EventHandler eventHandler, object sender, EventArgs args, string eventName = null) + { + eventHandler?.Invoke(sender, args); + } + + public void Dispatch(EventHandler eventHandler, object sender, TArgs args, string eventName = null) + { + eventHandler?.Invoke(sender, args); + } + + public void Dispatch(TypedEventHandler eventHandler, TSender sender, TArgs args, string eventName = null) + { + eventHandler?.Invoke(sender, args); + } + + public IEnumerable GetEvents(EventDefinitionFilter filter) + { + return Enumerable.Empty(); + } + + public void ScopeExit(bool completed) + { } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/PublishEventArgs.cs b/src/Umbraco.Core/Events/PublishEventArgs.cs index a791781617..1aa7c2308c 100644 --- a/src/Umbraco.Core/Events/PublishEventArgs.cs +++ b/src/Umbraco.Core/Events/PublishEventArgs.cs @@ -1,8 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Umbraco.Core.Events { - public class PublishEventArgs : CancellableObjectEventArgs> + public class PublishEventArgs : CancellableObjectEventArgs>, IEquatable> { /// /// Constructor accepting multiple entities that are used in the publish operation @@ -101,5 +102,38 @@ namespace Umbraco.Core.Events } public bool IsAllRepublished { get; private set; } + + public bool Equals(PublishEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && IsAllRepublished == other.IsAllRepublished; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((PublishEventArgs) obj); + } + + public override int GetHashCode() + { + unchecked + { + return (base.GetHashCode() * 397) ^ IsAllRepublished.GetHashCode(); + } + } + + public static bool operator ==(PublishEventArgs left, PublishEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(PublishEventArgs left, PublishEventArgs right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/QueuingEventDispatcher.cs b/src/Umbraco.Core/Events/QueuingEventDispatcher.cs new file mode 100644 index 0000000000..238c9568eb --- /dev/null +++ b/src/Umbraco.Core/Events/QueuingEventDispatcher.cs @@ -0,0 +1,42 @@ +using Umbraco.Core.DI; +using Umbraco.Core.IO; + +namespace Umbraco.Core.Events +{ + /// + /// This event manager is created for each scope and is aware of if it is nested in an outer scope + /// + /// + /// The outer scope is the only scope that can raise events, the inner scope's will defer to the outer scope + /// + internal class QueuingEventDispatcher : QueuingEventDispatcherBase + { + public QueuingEventDispatcher() + : base(true) + { } + + protected override void ScopeExitCompleted() + { + // processing only the last instance of each event... + // this is probably far from perfect, because if eg a content is saved in a list + // and then as a single content, the two events will probably not be de-duplicated, + // but it's better than nothing + + foreach (var e in GetEvents(EventDefinitionFilter.LastIn)) + { + e.RaiseEvent(); + + // separating concerns means that this should probably not be here, + // but then where should it be (without making things too complicated)? + var delete = e.Args as IDeletingMediaFilesEventArgs; + if (delete != null && delete.MediaFilesToDelete.Count > 0) + MediaFileSystem.DeleteMediaFiles(delete.MediaFilesToDelete); + } + } + + private MediaFileSystem _mediaFileSystem; + + // fixme inject + private MediaFileSystem MediaFileSystem => _mediaFileSystem ?? (_mediaFileSystem = Current.FileSystems.MediaFileSystem); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/QueuingEventDispatcherBase.cs b/src/Umbraco.Core/Events/QueuingEventDispatcherBase.cs new file mode 100644 index 0000000000..d3f804a50d --- /dev/null +++ b/src/Umbraco.Core/Events/QueuingEventDispatcherBase.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Events +{ + public abstract class QueuingEventDispatcherBase : IEventDispatcher + { + private List _events; + private readonly bool _raiseCancelable; + + protected QueuingEventDispatcherBase(bool raiseCancelable) + { + _raiseCancelable = raiseCancelable; + } + + private List Events => _events ?? (_events = new List()); + + public bool DispatchCancelable(EventHandler eventHandler, object sender, CancellableEventArgs args, string eventName = null) + { + if (eventHandler == null) return args.Cancel; + if (_raiseCancelable == false) return args.Cancel; + eventHandler(sender, args); + return args.Cancel; + } + + public bool DispatchCancelable(EventHandler eventHandler, object sender, TArgs args, string eventName = null) + where TArgs : CancellableEventArgs + { + if (eventHandler == null) return args.Cancel; + if (_raiseCancelable == false) return args.Cancel; + eventHandler(sender, args); + return args.Cancel; + } + + public bool DispatchCancelable(TypedEventHandler eventHandler, TSender sender, TArgs args, string eventName = null) + where TArgs : CancellableEventArgs + { + if (eventHandler == null) return args.Cancel; + if (_raiseCancelable == false) return args.Cancel; + eventHandler(sender, args); + return args.Cancel; + } + + public void Dispatch(EventHandler eventHandler, object sender, EventArgs args, string eventName = null) + { + if (eventHandler == null) return; + Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); + } + + public void Dispatch(EventHandler eventHandler, object sender, TArgs args, string eventName = null) + { + if (eventHandler == null) return; + Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); + } + + public void Dispatch(TypedEventHandler eventHandler, TSender sender, TArgs args, string eventName = null) + { + if (eventHandler == null) return; + Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); + } + + public IEnumerable GetEvents(EventDefinitionFilter filter) + { + if (_events == null) + return Enumerable.Empty(); + + switch (filter) + { + case EventDefinitionFilter.All: + return _events; + case EventDefinitionFilter.FirstIn: + var l1 = new OrderedHashSet(); + foreach (var e in _events) + { + l1.Add(e); + } + return l1; + case EventDefinitionFilter.LastIn: + var l2 = new OrderedHashSet(keepOldest: false); + foreach (var e in _events) + { + l2.Add(e); + } + return l2; + default: + throw new ArgumentOutOfRangeException(nameof(filter), filter, null); + } + } + + public void ScopeExit(bool completed) + { + if (_events == null) return; + if (completed) + ScopeExitCompleted(); + _events.Clear(); + } + + protected abstract void ScopeExitCompleted(); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/RecycleBinEventArgs.cs b/src/Umbraco.Core/Events/RecycleBinEventArgs.cs index 7818156207..811dea679e 100644 --- a/src/Umbraco.Core/Events/RecycleBinEventArgs.cs +++ b/src/Umbraco.Core/Events/RecycleBinEventArgs.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Models; namespace Umbraco.Core.Events { - public class RecycleBinEventArgs : CancellableEventArgs + public class RecycleBinEventArgs : CancellableEventArgs, IEquatable, IDeletingMediaFilesEventArgs { public RecycleBinEventArgs(Guid nodeObjectType, Dictionary> allPropertyData, bool emptiedSuccessfully) : base(false) @@ -116,6 +116,8 @@ namespace Umbraco.Core.Events /// public List Files { get; private set; } + public List MediaFilesToDelete { get { return Files; } } + /// /// Gets the list of all property data associated with a content id /// @@ -141,5 +143,44 @@ namespace Umbraco.Core.Events { get { return NodeObjectType == new Guid(Constants.ObjectTypes.Media); } } + + public bool Equals(RecycleBinEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && AllPropertyData.Equals(other.AllPropertyData) && Files.Equals(other.Files) && Ids.Equals(other.Ids) && NodeObjectType.Equals(other.NodeObjectType) && RecycleBinEmptiedSuccessfully == other.RecycleBinEmptiedSuccessfully; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((RecycleBinEventArgs) obj); + } + + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ AllPropertyData.GetHashCode(); + hashCode = (hashCode * 397) ^ Files.GetHashCode(); + hashCode = (hashCode * 397) ^ Ids.GetHashCode(); + hashCode = (hashCode * 397) ^ NodeObjectType.GetHashCode(); + hashCode = (hashCode * 397) ^ RecycleBinEmptiedSuccessfully.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(RecycleBinEventArgs left, RecycleBinEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(RecycleBinEventArgs left, RecycleBinEventArgs right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/ScopeLifetimeMessagesFactory.cs b/src/Umbraco.Core/Events/ScopeLifetimeMessagesFactory.cs new file mode 100644 index 0000000000..476789a30e --- /dev/null +++ b/src/Umbraco.Core/Events/ScopeLifetimeMessagesFactory.cs @@ -0,0 +1,60 @@ +// fixme - remove this file +//using System; +//using Umbraco.Core.Scoping; + +//namespace Umbraco.Core.Events +//{ +// /// +// /// Stores the instance of EventMessages in the current scope. +// /// +// internal class ScopeLifetimeMessagesFactory : IEventMessagesFactory +// { +// public const string ContextKey = "Umbraco.Core.Events.ScopeLifetimeMessagesFactory"; + +// // fixme for v8 that one will need to be entirely and massively refactored + +// private readonly IHttpContextAccessor _contextAccessor; +// private readonly IScopeProviderInternal _scopeProvider; + +// public static ScopeLifetimeMessagesFactory Current { get; private set; } + +// public ScopeLifetimeMessagesFactory(IHttpContextAccessor contextAccesor, IScopeProvider scopeProvider) +// { +// if (scopeProvider == null) throw new ArgumentNullException(nameof(scopeProvider)); +// if (scopeProvider is IScopeProviderInternal == false) throw new ArgumentException("Not IScopeProviderInternal.", nameof(scopeProvider)); +// _contextAccessor = contextAccesor ?? throw new ArgumentNullException(nameof(contextAccesor)); +// _scopeProvider = (IScopeProviderInternal) scopeProvider; +// Current = this; +// } + +// public EventMessages Get() +// { +// var messages = GetFromHttpContext(); +// if (messages != null) return messages; + +// var scope = _scopeProvider.GetAmbientOrNoScope(); +// return scope.Messages; +// } + +// public EventMessages GetFromHttpContext() +// { +// if (_contextAccessor == null || _contextAccessor.Value == null) return null; +// return (EventMessages)_contextAccessor.Value.Items[ContextKey]; +// } + +// public EventMessages TryGet() +// { +// var messages = GetFromHttpContext(); +// if (messages != null) return messages; + +// var scope = _scopeProvider.AmbientScope; +// return scope?.MessagesOrNull; +// } + +// public void Set(EventMessages messages) +// { +// if (_contextAccessor.Value == null) return; +// _contextAccessor.Value.Items[ContextKey] = messages; +// } +// } +//} diff --git a/src/Umbraco.Core/Events/UninstallPackageEventArgs.cs b/src/Umbraco.Core/Events/UninstallPackageEventArgs.cs new file mode 100644 index 0000000000..dcbaba550e --- /dev/null +++ b/src/Umbraco.Core/Events/UninstallPackageEventArgs.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Umbraco.Core.Models.Packaging; + +namespace Umbraco.Core.Events +{ + internal class UninstallPackageEventArgs : CancellableObjectEventArgs> + { + public UninstallPackageEventArgs(TEntity eventObject, bool canCancel) + : base(new[] { eventObject }, canCancel) + { } + + public UninstallPackageEventArgs(TEntity eventObject, MetaData packageMetaData) + : base(new[] { eventObject }) + { + PackageMetaData = packageMetaData; + } + + public MetaData PackageMetaData { get; } + + public IEnumerable UninstallationSummary => EventObject; + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Exceptions/LightInjectException.cs b/src/Umbraco.Core/Exceptions/LightInjectException.cs new file mode 100644 index 0000000000..1e2a01efd8 --- /dev/null +++ b/src/Umbraco.Core/Exceptions/LightInjectException.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Umbraco.Core.Exceptions +{ + /// + /// Represents errors that occur due to LightInject. + /// + public class LightInjectException : Exception + { + public LightInjectException(string message) + : base(message) + { } + + public LightInjectException(string message, Exception innerException) + : base(message, innerException) + { } + + private const string LightInjectUnableToResolveType = "Unable to resolve type:"; + private const string LightInjectUnresolvedDependency = "Unresolved dependency "; + private const string LightInjectRequestedDependency = "[Requested dependency:"; + + public static void TryThrow(Exception e) + { + var ex = e as InvalidOperationException; + if (ex == null || ex.Message.StartsWith(LightInjectUnableToResolveType) == false) + return; + + var messages = new List { ex.Message }; + + ex = ex.InnerException as InvalidOperationException; + while (ex != null) + { + messages.Add(ex.Message); + ex = ex.InnerException as InvalidOperationException; + } + + var sb = new StringBuilder(); + var last = messages.Last(); + if (last.StartsWith(LightInjectUnableToResolveType)) + { + sb.AppendLine("Unresolved type: " + last.Substring(LightInjectUnableToResolveType.Length)); + } + else if (last.StartsWith(LightInjectUnresolvedDependency)) + { + var pos = last.InvariantIndexOf(LightInjectRequestedDependency); + sb.AppendLine("Unresolved dependency: " + last.Substring(pos + LightInjectRequestedDependency.Length + 1).TrimEnd(']')); + sb.AppendLine(" (see inner exceptions for the entire dependencies chain)."); + } + else return; // wtf? + + throw new LightInjectException(sb.ToString(), e); + } + } +} diff --git a/src/Umbraco.Core/Exceptions/WontImplementException.cs b/src/Umbraco.Core/Exceptions/WontImplementException.cs new file mode 100644 index 0000000000..7774bf53de --- /dev/null +++ b/src/Umbraco.Core/Exceptions/WontImplementException.cs @@ -0,0 +1,27 @@ +using System; + +namespace Umbraco.Core.Exceptions +{ + /// + /// The exception that is thrown when a requested method or operation is not, and will not be, implemented. + /// + /// The is to be used when some code is not implemented, + /// but should eventually be implemented (i.e. work in progress) and is reported by tools such as ReSharper. + /// This exception is to be used when some code is not implemented, and is not meant to be, for whatever + /// reason. + public class WontImplementException : NotImplementedException + { + /// + /// Initializes a new instance of the class. + /// + public WontImplementException() + { } + + /// + /// Initializes a new instance of the class with a specified reason message. + /// + public WontImplementException(string message) + : base(message) + { } + } +} diff --git a/src/Umbraco.Core/GuidUdi.cs b/src/Umbraco.Core/GuidUdi.cs new file mode 100644 index 0000000000..7a740ef58f --- /dev/null +++ b/src/Umbraco.Core/GuidUdi.cs @@ -0,0 +1,85 @@ +using System; +using System.ComponentModel; + +namespace Umbraco.Core +{ + /// + /// Represents a guid-based entity identifier. + /// + [TypeConverter(typeof(UdiTypeConverter))] + public class GuidUdi : Udi + { + /// + /// The guid part of the identifier. + /// + public Guid Guid { get; private set; } + + /// + /// Initializes a new instance of the GuidUdi class with an entity type and a guid. + /// + /// The entity type part of the udi. + /// The guid part of the udi. + public GuidUdi(string entityType, Guid guid) + : base(entityType, "umb://" + entityType + "/" + guid.ToString("N")) + { + Guid = guid; + } + + /// + /// Initializes a new instance of the GuidUdi class with an uri value. + /// + /// The uri value of the udi. + public GuidUdi(Uri uriValue) + : base(uriValue) + { + Guid = Guid.Parse(uriValue.AbsolutePath.TrimStart('/')); + } + + /// + /// Converts the string representation of an entity identifier into the equivalent GuidUdi instance. + /// + /// The string to convert. + /// A GuidUdi instance that contains the value that was parsed. + public new static GuidUdi Parse(string s) + { + var udi = Udi.Parse(s); + if (!(udi is GuidUdi)) + throw new FormatException("String \"" + s + "\" is not a guid entity id."); + return (GuidUdi)udi; + } + + public static bool TryParse(string s, out GuidUdi udi) + { + Udi tmp; + udi = null; + if (!TryParse(s, out tmp)) return false; + udi = tmp as GuidUdi; + return udi != null; + } + + public override bool Equals(object obj) + { + var other = obj as GuidUdi; + if (other == null) return false; + return EntityType == other.EntityType && Guid == other.Guid; + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + /// + public override bool IsRoot + { + get { return Guid == Guid.Empty; } + } + + /// + public GuidUdi EnsureClosed() + { + base.EnsureNotRoot(); + return this; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/IO/FileSystemExtensions.cs b/src/Umbraco.Core/IO/FileSystemExtensions.cs index 1fae296c5f..7946a11528 100644 --- a/src/Umbraco.Core/IO/FileSystemExtensions.cs +++ b/src/Umbraco.Core/IO/FileSystemExtensions.cs @@ -6,7 +6,6 @@ namespace Umbraco.Core.IO { public static class FileSystemExtensions { - /// /// Attempts to open the file at filePath up to maxRetries times, /// with a thread sleep time of sleepPerRetryInMilliseconds between retries. diff --git a/src/Umbraco.Core/IO/FileSystemWrapper.cs b/src/Umbraco.Core/IO/FileSystemWrapper.cs index 54eaf767e2..918883823e 100644 --- a/src/Umbraco.Core/IO/FileSystemWrapper.cs +++ b/src/Umbraco.Core/IO/FileSystemWrapper.cs @@ -103,10 +103,16 @@ namespace Umbraco.Core.IO return Wrapped.GetCreated(path); } - // explicitely implementing - not breaking - long IFileSystem.GetSize(string path) + public long GetSize(string path) { return Wrapped.GetSize(path); } - } + + public bool CanAddPhysical => Wrapped.CanAddPhysical; + + public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false) + { + Wrapped.AddFile(path, physicalPath, overrideIfExists, copy); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/IO/FileSystems.cs b/src/Umbraco.Core/IO/FileSystems.cs index 0a57ad2c63..946c1e3b90 100644 --- a/src/Umbraco.Core/IO/FileSystems.cs +++ b/src/Umbraco.Core/IO/FileSystems.cs @@ -7,66 +7,84 @@ using System.Reflection; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Plugins; +using Umbraco.Core.Scoping; namespace Umbraco.Core.IO -{ +{ public class FileSystems { + private readonly IScopeProviderInternal _scopeProvider; private readonly FileSystemProvidersSection _config; - private readonly WeakSet _wrappers = new WeakSet(); + private readonly ConcurrentSet _wrappers = new ConcurrentSet(); private readonly ILogger _logger; + private readonly ConcurrentDictionary _providerLookup = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _filesystems = new ConcurrentDictionary(); + // wrappers for shadow support - private readonly ShadowWrapper _macroPartialFileSystemWrapper; - private readonly ShadowWrapper _partialViewsFileSystemWrapper; - private readonly ShadowWrapper _stylesheetsFileSystemWrapper; - private readonly ShadowWrapper _scriptsFileSystemWrapper; - private readonly ShadowWrapper _xsltFileSystemWrapper; - private readonly ShadowWrapper _masterPagesFileSystemWrapper; - private readonly ShadowWrapper _mvcViewsFileSystemWrapper; + private ShadowWrapper _macroPartialFileSystem; + private ShadowWrapper _partialViewsFileSystem; + private ShadowWrapper _stylesheetsFileSystem; + private ShadowWrapper _scriptsFileSystem; + private ShadowWrapper _xsltFileSystem; + private ShadowWrapper _masterPagesFileSystem; + private ShadowWrapper _mvcViewsFileSystem; - #region Singleton & Constructor + #region Constructor - public FileSystems(ILogger logger) + // fixme - circular dependency on scope provider, refactor! + + internal FileSystems(IScopeProviderInternal scopeProvider, ILogger logger) { _config = (FileSystemProvidersSection) ConfigurationManager.GetSection("umbracoConfiguration/FileSystemProviders"); + _scopeProvider = scopeProvider; _logger = logger; + CreateWellKnownFileSystems(); + } - // create the filesystems - MacroPartialsFileSystem = new PhysicalFileSystem(SystemDirectories.MacroPartials); - PartialViewsFileSystem = new PhysicalFileSystem(SystemDirectories.PartialViews); - StylesheetsFileSystem = new PhysicalFileSystem(SystemDirectories.Css); - ScriptsFileSystem = new PhysicalFileSystem(SystemDirectories.Scripts); - XsltFileSystem = new PhysicalFileSystem(SystemDirectories.Xslt); - MasterPagesFileSystem = new PhysicalFileSystem(SystemDirectories.Masterpages); - MvcViewsFileSystem = new PhysicalFileSystem(SystemDirectories.MvcViews); - - // wrap the filesystems for shadow support - MacroPartialsFileSystem = _macroPartialFileSystemWrapper = new ShadowWrapper(MacroPartialsFileSystem, "Views/MacroPartials"); - PartialViewsFileSystem = _partialViewsFileSystemWrapper = new ShadowWrapper(PartialViewsFileSystem, "Views/Partials"); - StylesheetsFileSystem = _stylesheetsFileSystemWrapper = new ShadowWrapper(StylesheetsFileSystem, "css"); - ScriptsFileSystem = _scriptsFileSystemWrapper = new ShadowWrapper(ScriptsFileSystem, "scripts"); - XsltFileSystem = _xsltFileSystemWrapper = new ShadowWrapper(XsltFileSystem, "xslt"); - MasterPagesFileSystem = _masterPagesFileSystemWrapper = new ShadowWrapper(MasterPagesFileSystem, "masterpages"); - MvcViewsFileSystem = _mvcViewsFileSystemWrapper = new ShadowWrapper(MvcViewsFileSystem, "Views"); - - // obtain filesystems from GetFileSystem - // these are already wrapped and do not need to be wrapped again - MediaFileSystem = GetFileSystem(); + // for tests only, totally unsafe + internal void Reset() + { + _wrappers.Clear(); + _providerLookup.Clear(); + _filesystems.Clear(); + CreateWellKnownFileSystems(); } #endregion #region Well-Known FileSystems - public IFileSystem MacroPartialsFileSystem { get; } - public IFileSystem PartialViewsFileSystem { get; } - public IFileSystem StylesheetsFileSystem { get; } - public IFileSystem ScriptsFileSystem { get; } - public IFileSystem XsltFileSystem { get; } - public IFileSystem MasterPagesFileSystem { get; } - public IFileSystem MvcViewsFileSystem { get; } - public MediaFileSystem MediaFileSystem { get; } + public IFileSystem MacroPartialsFileSystem => _macroPartialFileSystem; + public IFileSystem PartialViewsFileSystem => _partialViewsFileSystem; + public IFileSystem StylesheetsFileSystem => _stylesheetsFileSystem; + public IFileSystem ScriptsFileSystem => _scriptsFileSystem; + public IFileSystem XsltFileSystem => _xsltFileSystem; + public IFileSystem MasterPagesFileSystem => _masterPagesFileSystem; // fixme - see 7.6?! + public IFileSystem MvcViewsFileSystem => _mvcViewsFileSystem; + public MediaFileSystem MediaFileSystem { get; private set; } + + private void CreateWellKnownFileSystems() + { + var macroPartialFileSystem = new PhysicalFileSystem(SystemDirectories.MacroPartials); + var partialViewsFileSystem = new PhysicalFileSystem(SystemDirectories.PartialViews); + var stylesheetsFileSystem = new PhysicalFileSystem(SystemDirectories.Css); + var scriptsFileSystem = new PhysicalFileSystem(SystemDirectories.Scripts); + var xsltFileSystem = new PhysicalFileSystem(SystemDirectories.Xslt); + var masterPagesFileSystem = new PhysicalFileSystem(SystemDirectories.Masterpages); + var mvcViewsFileSystem = new PhysicalFileSystem(SystemDirectories.MvcViews); + + _macroPartialFileSystem = new ShadowWrapper(macroPartialFileSystem, "Views/MacroPartials", _scopeProvider); + _partialViewsFileSystem = new ShadowWrapper(partialViewsFileSystem, "Views/Partials", _scopeProvider); + _stylesheetsFileSystem = new ShadowWrapper(stylesheetsFileSystem, "css", _scopeProvider); + _scriptsFileSystem = new ShadowWrapper(scriptsFileSystem, "scripts", _scopeProvider); + _xsltFileSystem = new ShadowWrapper(xsltFileSystem, "xslt", _scopeProvider); + _masterPagesFileSystem = new ShadowWrapper(masterPagesFileSystem, "masterpages", _scopeProvider); + _mvcViewsFileSystem = new ShadowWrapper(mvcViewsFileSystem, "Views", _scopeProvider); + + // filesystems obtained from GetFileSystemProvider are already wrapped and do not need to be wrapped again + MediaFileSystem = GetFileSystemProvider(); + } #endregion @@ -82,56 +100,74 @@ namespace Umbraco.Core.IO //public string ProviderAlias { get; set; } } - private readonly ConcurrentDictionary _providerLookup = new ConcurrentDictionary(); - private readonly ConcurrentDictionary _aliases = new ConcurrentDictionary(); - /// /// Gets an underlying (non-typed) filesystem supporting a strongly-typed filesystem. /// /// The alias of the strongly-typed filesystem. /// The non-typed filesystem supporting the strongly-typed filesystem with the specified alias. - /// This method should not be used directly, used instead. - internal IFileSystem GetUnderlyingFileSystemProvider(string alias) + /// This method should not be used directly, used instead. + public IFileSystem GetUnderlyingFileSystemProvider(string alias) { - // either get the constructor info from cache or create it and add to cache - var ctorInfo = _providerLookup.GetOrAdd(alias, s => - { - // get config - var providerConfig = _config.Providers[s]; - if (providerConfig == null) - throw new ArgumentException($"No provider found with alias {s}."); + return GetUnderlyingFileSystemProvider(alias, null); + } - // get the filesystem type - var providerType = Type.GetType(providerConfig.Type); - if (providerType == null) - throw new InvalidOperationException($"Could not find type {providerConfig.Type}."); + /// + /// Gets an underlying (non-typed) filesystem supporting a strongly-typed filesystem. + /// + /// The alias of the strongly-typed filesystem. + /// A fallback creator for the filesystem. + /// The non-typed filesystem supporting the strongly-typed filesystem with the specified alias. + /// This method should not be used directly, used instead. + internal IFileSystem GetUnderlyingFileSystemProvider(string alias, Func fallback) + { + // either get the constructor info from cache or create it and add to cache + var ctorInfo = _providerLookup.GetOrAdd(alias, _ => GetUnderlyingFileSystemCtor(alias, fallback)); + return ctorInfo == null ? fallback() : (IFileSystem) ctorInfo.Constructor.Invoke(ctorInfo.Parameters); + } - // ensure it implements IFileSystem - if (providerType.IsAssignableFrom(typeof (IFileSystem))) - throw new InvalidOperationException($"Type {providerType.FullName} does not implement IFileSystem."); + private IFileSystem GetUnderlyingFileSystemNoCache(string alias, Func fallback) + { + var ctorInfo = GetUnderlyingFileSystemCtor(alias, fallback); + return ctorInfo == null ? fallback() : (IFileSystem) ctorInfo.Constructor.Invoke(ctorInfo.Parameters); + } - // find a ctor matching the config parameters - var paramCount = providerConfig.Parameters?.Count ?? 0; - var constructor = providerType.GetConstructors().SingleOrDefault(x - => x.GetParameters().Length == paramCount && x.GetParameters().All(y => providerConfig.Parameters.AllKeys.Contains(y.Name))); - if (constructor == null) - throw new InvalidOperationException($"Type {providerType.FullName} has no ctor matching the {paramCount} configuration parameter(s)."); + private ProviderConstructionInfo GetUnderlyingFileSystemCtor(string alias, Func fallback) + { + // get config + var providerConfig = _config.Providers[alias]; + if (providerConfig == null) + { + if (fallback != null) return null; + throw new ArgumentException($"No provider found with alias {alias}."); + } - var parameters = new object[paramCount]; - if (providerConfig.Parameters != null) // keeps ReSharper happy - for (var i = 0; i < paramCount; i++) - parameters[i] = providerConfig.Parameters[providerConfig.Parameters.AllKeys[i]].Value; + // get the filesystem type + var providerType = Type.GetType(providerConfig.Type); + if (providerType == null) + throw new InvalidOperationException($"Could not find type {providerConfig.Type}."); - return new ProviderConstructionInfo - { - Constructor = constructor, - Parameters = parameters, - //ProviderAlias = s - }; - }); + // ensure it implements IFileSystem + if (providerType.IsAssignableFrom(typeof (IFileSystem))) + throw new InvalidOperationException($"Type {providerType.FullName} does not implement IFileSystem."); - // create the fs and return - return (IFileSystem) ctorInfo.Constructor.Invoke(ctorInfo.Parameters); + // find a ctor matching the config parameters + var paramCount = providerConfig.Parameters?.Count ?? 0; + var constructor = providerType.GetConstructors().SingleOrDefault(x + => x.GetParameters().Length == paramCount && x.GetParameters().All(y => providerConfig.Parameters.AllKeys.Contains(y.Name))); + if (constructor == null) + throw new InvalidOperationException($"Type {providerType.FullName} has no ctor matching the {paramCount} configuration parameter(s)."); + + var parameters = new object[paramCount]; + if (providerConfig.Parameters != null) // keeps ReSharper happy + for (var i = 0; i < paramCount; i++) + parameters[i] = providerConfig.Parameters[providerConfig.Parameters.AllKeys[i]].Value; + + return new ProviderConstructionInfo + { + Constructor = constructor, + Parameters = parameters, + //ProviderAlias = s + }; } /// @@ -139,38 +175,69 @@ namespace Umbraco.Core.IO /// /// The type of the filesystem. /// A strongly-typed filesystem of the specified type. - public TFileSystem GetFileSystem() - where TFileSystem : FileSystemWrapper + /// + /// Ideally, this should cache the instances, but that would break backward compatibility, so we + /// only do it for our own MediaFileSystem - for everything else, it's the responsibility of the caller + /// to ensure that they maintain singletons. This is important for singletons, as each filesystem maintains + /// its own shadow and having multiple instances would lead to inconsistencies. + /// Note that any filesystem created by this method *after* shadowing begins, will *not* be + /// shadowing (and an exception will be thrown by the ShadowWrapper). + /// + // fixme - should it change for v8? + public TFileSystem GetFileSystemProvider() + where TFileSystem : FileSystemWrapper { - // deal with known types - avoid infinite loops! - if (typeof(TFileSystem) == typeof(MediaFileSystem) && MediaFileSystem != null) - return MediaFileSystem as TFileSystem; // else create and return + return GetFileSystemProvider(null); + } - // get/cache the alias for the filesystem type - var alias = _aliases.GetOrAdd(typeof (TFileSystem), fsType => - { - // validate the ctor - var constructor = fsType.GetConstructors().SingleOrDefault(x - => x.GetParameters().Length == 1 && TypeHelper.IsTypeAssignableFrom(x.GetParameters().Single().ParameterType)); - if (constructor == null) - throw new InvalidOperationException("Type " + fsType.FullName + " must inherit from FileSystemWrapper and have a constructor that accepts one parameter of type " + typeof(IFileSystem).FullName + "."); + /// + /// Gets a strongly-typed filesystem. + /// + /// The type of the filesystem. + /// A fallback creator for the inner filesystem. + /// A strongly-typed filesystem of the specified type. + /// + /// The fallback creator is used only if nothing is configured. + /// Ideally, this should cache the instances, but that would break backward compatibility, so we + /// only do it for our own MediaFileSystem - for everything else, it's the responsibility of the caller + /// to ensure that they maintain singletons. This is important for singletons, as each filesystem maintains + /// its own shadow and having multiple instances would lead to inconsistencies. + /// Note that any filesystem created by this method *after* shadowing begins, will *not* be + /// shadowing (and an exception will be thrown by the ShadowWrapper). + /// + public TFileSystem GetFileSystemProvider(Func fallback) + where TFileSystem : FileSystemWrapper + { + var alias = GetFileSystemAlias(); + return (TFileSystem)_filesystems.GetOrAdd(alias, _ => + { + // gets the inner fs, create the strongly-typed fs wrapping the inner fs, register & return + // so we are double-wrapping here + // could be optimized by having FileSystemWrapper inherit from ShadowWrapper, maybe + var innerFs = GetUnderlyingFileSystemNoCache(alias, fallback); + var shadowWrapper = new ShadowWrapper(innerFs, "typed/" + alias, _scopeProvider); + var fs = (IFileSystem) Activator.CreateInstance(typeof(TFileSystem), shadowWrapper); + _wrappers.Add(shadowWrapper); // keeping a reference to the wrapper + return fs; + }); + } - // find the attribute and get the alias - var attr = (FileSystemProviderAttribute) fsType.GetCustomAttributes(typeof(FileSystemProviderAttribute), false).SingleOrDefault(); - if (attr == null) - throw new InvalidOperationException("Type " + fsType.FullName + "is missing the required FileSystemProviderAttribute."); + private string GetFileSystemAlias() + { + var fsType = typeof(TFileSystem); - return attr.Alias; - }); + // validate the ctor + var constructor = fsType.GetConstructors().SingleOrDefault(x + => x.GetParameters().Length == 1 && TypeHelper.IsTypeAssignableFrom(x.GetParameters().Single().ParameterType)); + if (constructor == null) + throw new InvalidOperationException("Type " + fsType.FullName + " must inherit from FileSystemWrapper and have a constructor that accepts one parameter of type " + typeof(IFileSystem).FullName + "."); - // gets the inner fs, create the strongly-typed fs wrapping the inner fs, register & return - // so we are double-wrapping here - // could be optimized by having FileSystemWrapper inherit from ShadowWrapper, maybe - var innerFs = GetUnderlyingFileSystemProvider(alias); - var shadowWrapper = new ShadowWrapper(innerFs, "typed/" + alias); - var fs = (TFileSystem) Activator.CreateInstance(typeof (TFileSystem), innerFs); - _wrappers.Add(shadowWrapper); // keeping a weak reference to the wrapper - return fs; + // find the attribute and get the alias + var attr = (FileSystemProviderAttribute)fsType.GetCustomAttributes(typeof(FileSystemProviderAttribute), false).SingleOrDefault(); + if (attr == null) + throw new InvalidOperationException("Type " + fsType.FullName + "is missing the required FileSystemProviderAttribute."); + + return attr.Alias; } #endregion @@ -195,36 +262,43 @@ namespace Umbraco.Core.IO // _shadowEnabled = true; //} - public ICompletable Shadow(Guid id) + internal ICompletable Shadow(Guid id) { var typed = _wrappers.ToArray(); var wrappers = new ShadowWrapper[typed.Length + 7]; var i = 0; while (i < typed.Length) wrappers[i] = typed[i++]; - wrappers[i++] = _macroPartialFileSystemWrapper; - wrappers[i++] = _partialViewsFileSystemWrapper; - wrappers[i++] = _stylesheetsFileSystemWrapper; - wrappers[i++] = _scriptsFileSystemWrapper; - wrappers[i++] = _xsltFileSystemWrapper; - wrappers[i++] = _masterPagesFileSystemWrapper; - wrappers[i] = _mvcViewsFileSystemWrapper; + wrappers[i++] = _macroPartialFileSystem; + wrappers[i++] = _partialViewsFileSystem; + wrappers[i++] = _stylesheetsFileSystem; + wrappers[i++] = _scriptsFileSystem; + wrappers[i++] = _xsltFileSystem; + wrappers[i++] = _masterPagesFileSystem; + wrappers[i] = _mvcViewsFileSystem; - return ShadowFileSystemsScope.CreateScope(id, wrappers, _logger); + return new ShadowFileSystems(id, wrappers, _logger); } #endregion - private class WeakSet + private class ConcurrentSet where T : class { - private readonly HashSet> _set = new HashSet>(); + private readonly HashSet _set = new HashSet(); public void Add(T item) { lock (_set) { - _set.Add(new WeakReference(item)); - CollectLocked(); + _set.Add(item); + } + } + + public void Clear() + { + lock (_set) + { + _set.Clear(); } } @@ -232,23 +306,9 @@ namespace Umbraco.Core.IO { lock (_set) { - CollectLocked(); - return _set.Select(x => - { - T target; - return x.TryGetTarget(out target) ? target : null; - }).WhereNotNull().ToArray(); + return _set.ToArray(); } } - - private void CollectLocked() - { - _set.RemoveWhere(x => - { - T target; - return x.TryGetTarget(out target) == false; - }); - } } } } diff --git a/src/Umbraco.Core/IO/IFileSystem.cs b/src/Umbraco.Core/IO/IFileSystem.cs index 280a5f521e..0baedfd479 100644 --- a/src/Umbraco.Core/IO/IFileSystem.cs +++ b/src/Umbraco.Core/IO/IFileSystem.cs @@ -4,42 +4,170 @@ using System.IO; namespace Umbraco.Core.IO { - public interface IFileSystem + /// + /// Provides methods allowing the manipulation of files within an Umbraco application. + /// + public interface IFileSystem { + /// + /// Gets all directories matching the given path. + /// + /// The path to the directories. + /// + /// The representing the matched directories. + /// IEnumerable GetDirectories(string path); + /// + /// Deletes the specified directory. + /// + /// The name of the directory to remove. void DeleteDirectory(string path); + /// + /// Deletes the specified directory and, if indicated, any subdirectories and files in the directory. + /// + /// Azure blob storage has no real concept of directories so deletion is always recursive. + /// The name of the directory to remove. + /// Whether to remove directories, subdirectories, and files in path. void DeleteDirectory(string path, bool recursive); + /// + /// Determines whether the specified directory exists. + /// + /// The directory to check. + /// + /// True if the directory exists and the user has permission to view it; otherwise false. + /// bool DirectoryExists(string path); + /// + /// Adds a file to the file system. + /// + /// The path to the given file. + /// The containing the file contents. void AddFile(string path, Stream stream); + /// + /// Adds a file to the file system. + /// + /// The path to the given file. + /// The containing the file contents. + /// Whether to override the file if it already exists. void AddFile(string path, Stream stream, bool overrideIfExists); + /// + /// Gets all files matching the given path. + /// + /// The path to the files. + /// + /// The representing the matched files. + /// IEnumerable GetFiles(string path); + /// + /// Gets all files matching the given path and filter. + /// + /// The path to the files. + /// A filter that allows the querying of file extension. *.jpg + /// + /// The representing the matched files. + /// IEnumerable GetFiles(string path, string filter); + /// + /// Gets a representing the file at the gieven path. + /// + /// The path to the file. + /// + /// . + /// Stream OpenFile(string path); + /// + /// Deletes the specified file. + /// + /// The name of the file to remove. void DeleteFile(string path); + /// + /// Determines whether the specified file exists. + /// + /// The file to check. + /// + /// True if the file exists and the user has permission to view it; otherwise false. + /// bool FileExists(string path); + /// + /// Returns the application relative path to the file. + /// + /// The full path or url. + /// + /// The representing the relative path. + /// string GetRelativePath(string fullPathOrUrl); + /// + /// Gets the full qualified path to the file. + /// + /// The file to return the full path for. + /// + /// The representing the full path. + /// string GetFullPath(string path); + /// + /// Returns the application relative url to the file. + /// + /// The path to return the url for. + /// + /// representing the relative url. + /// string GetUrl(string path); + /// + /// Gets the last modified date/time of the file, expressed as a UTC value. + /// + /// The path to the file. + /// + /// . + /// DateTimeOffset GetLastModified(string path); + /// + /// Gets the created date/time of the file, expressed as a UTC value. + /// + /// The path to the file. + /// + /// . + /// DateTimeOffset GetCreated(string path); + /// + /// Gets the size of a file. + /// + /// The path to the file. + /// The size (in bytes) of the file. long GetSize(string path); + /// + /// Gets a value indicating whether the filesystem can add/copy + /// a file which is on a physical filesystem. + /// + /// In other words, whether the filesystem can copy/move a file + /// that is on local disk, in a fast and efficient way. + bool CanAddPhysical { get; } + + /// + /// Adds a file which is on a physical filesystem. + /// + /// The path to the file. + /// The absolute physical path to the source file. + /// A value indicating what to do if the file already exists. + /// A value indicating whether to move (default) or copy. + void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false); + // TODO: implement these // //void CreateDirectory(string path); diff --git a/src/Umbraco.Core/IO/IOHelper.cs b/src/Umbraco.Core/IO/IOHelper.cs index 96b9927978..4ef240f73c 100644 --- a/src/Umbraco.Core/IO/IOHelper.cs +++ b/src/Umbraco.Core/IO/IOHelper.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Reflection; using System.IO; using System.Configuration; +using System.Linq; using System.Web; using System.Web.Hosting; using ICSharpCode.SharpZipLib.Zip; @@ -348,5 +349,52 @@ namespace Umbraco.Core.IO writer.Write(contents); } } + + /// + /// Checks if a given path is a full path including drive letter + /// + /// + /// + // From: http://stackoverflow.com/a/35046453/5018 + internal static bool IsFullPath(this string path) + { + return string.IsNullOrWhiteSpace(path) == false + && path.IndexOfAny(Path.GetInvalidPathChars().ToArray()) == -1 + && Path.IsPathRooted(path) + && Path.GetPathRoot(path).Equals(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal) == false; + } + + /// + /// Get properly formatted relative path from an existing absolute or relative path + /// + /// + /// + internal static string GetRelativePath(this string path) + { + if (path.IsFullPath()) + { + var rootDirectory = GetRootDirectorySafe(); + var relativePath = path.ToLowerInvariant().Replace(rootDirectory.ToLowerInvariant(), string.Empty); + path = relativePath; + } + + return path.EnsurePathIsApplicationRootPrefixed(); + } + + /// + /// Ensures that a path has `~/` as prefix + /// + /// + /// + internal static string EnsurePathIsApplicationRootPrefixed(this string path) + { + if (path.StartsWith("~/")) + return path; + if (path.StartsWith("/") == false && path.StartsWith("\\") == false) + path = string.Format("/{0}", path); + if (path.StartsWith("~") == false) + path = string.Format("~{0}", path); + return path; + } } } diff --git a/src/Umbraco.Core/IO/MediaFileSystem.cs b/src/Umbraco.Core/IO/MediaFileSystem.cs index e8afdafb75..591626e55b 100644 --- a/src/Umbraco.Core/IO/MediaFileSystem.cs +++ b/src/Umbraco.Core/IO/MediaFileSystem.cs @@ -31,11 +31,12 @@ namespace Umbraco.Core.IO private long _folderCounter; private bool _folderCounterInitialized; - private static readonly Dictionary DefaultSizes = new Dictionary - { - { 100, "thumb" }, - { 500, "big-thumb" } - }; + // fixme - remove + //private static readonly Dictionary DefaultSizes = new Dictionary + //{ + // { 100, "thumb" }, + // { 500, "big-thumb" } + //}; public MediaFileSystem(IFileSystem wrapped) : base(wrapped) @@ -95,7 +96,7 @@ namespace Umbraco.Core.IO } else { - DeleteFile(file, true); + DeleteFile(file); } } catch (Exception e) @@ -108,6 +109,41 @@ namespace Umbraco.Core.IO return allsuccess; } + public void DeleteMediaFiles(IEnumerable files) + { + files = files.Distinct(); + + Parallel.ForEach(files, file => + { + try + { + if (file.IsNullOrWhiteSpace()) return; + if (FileExists(file) == false) return; + DeleteFile(file); + + if (UseTheNewMediaPathScheme == false) + { + // old scheme: filepath is "/" OR "-" + // remove the directory if any + var dir = Path.GetDirectoryName(file); + if (string.IsNullOrWhiteSpace(dir) == false) + DeleteDirectory(dir, true); + } + else + { + // new scheme: path is "/" where xuid is a combination of cuid and puid + // remove the directory + var dir = Path.GetDirectoryName(file); + DeleteDirectory(dir, true); + } + } + catch (Exception e) + { + Logger.Error("Failed to delete attached file \"" + file + "\".", e); + } + }); + } + #region Media Path /// @@ -133,7 +169,7 @@ namespace Umbraco.Core.IO } else { - // new scheme: path is "-/" OR "--" + // new scheme: path is "/" where xuid is a combination of cuid and puid // default media filesystem maps to "~/media/" // assumes that cuid and puid keys can be trusted - and that a single property type // for a single content cannot store two different files with the same name @@ -255,7 +291,7 @@ namespace Umbraco.Core.IO // clear the old file, if any if (string.IsNullOrWhiteSpace(oldpath) == false) - DeleteFile(oldpath, true); + DeleteFile(oldpath); // get the filepath, store the data // use oldpath as "prevpath" to try and reuse the folder, in original number-based scheme @@ -264,15 +300,6 @@ namespace Umbraco.Core.IO return filepath; } - /// - /// Clears a media file. - /// - /// The filesystem-relative path to the media file. - public new void DeleteFile(string filepath) - { - DeleteFile(filepath, true); - } - /// /// Copies a media file as a new media file, associated to a property of a content item. /// @@ -293,7 +320,6 @@ namespace Umbraco.Core.IO var filename = Path.GetFileName(sourcepath); var filepath = GetMediaPath(filename, content.Key, propertyType.Key); this.CopyFile(sourcepath, filepath); - CopyThumbnails(sourcepath, filepath); return filepath; } @@ -331,7 +357,7 @@ namespace Umbraco.Core.IO var svalue = property.Value as string; var oldpath = svalue == null ? null : GetRelativePath(svalue); // FIXME DELETE? if (string.IsNullOrWhiteSpace(oldpath) == false && oldpath != filepath) - DeleteFile(oldpath, true); + DeleteFile(oldpath); property.Value = GetUrl(filepath); using (var filestream = OpenFile(filepath)) { @@ -343,25 +369,8 @@ namespace Umbraco.Core.IO // ie generates thumbnails and populates autofill properties private void SetUploadFile(IContentBase content, Property property, string filepath, Stream filestream) { - // check if file is an image (and supports resizing and thumbnails etc) - var extension = Path.GetExtension(filepath); - var isImage = IsImageFile(extension); - - // specific stuff for images (thumbnails etc) - if (isImage) - { - using (var image = Image.FromStream(filestream)) - { - // use one image for all - GenerateThumbnails(image, filepath, property.PropertyType); - UploadAutoFillProperties.Populate(content, property.Alias, filepath, filestream, image); - } - } - else - { - // will use filepath for extension, and filestream for length - UploadAutoFillProperties.Populate(content, property.Alias, filepath, filestream); - } + // will use filepath for extension, and filestream for length + UploadAutoFillProperties.Populate(content, property.Alias, filepath, filestream); } #endregion @@ -424,279 +433,281 @@ namespace Umbraco.Core.IO #endregion - #region Manage thumbnails + // fixme - remove - // note: this does not find 'custom' thumbnails? - // will find _thumb and _big-thumb but NOT _custom? - public IEnumerable GetThumbnails(string path) - { - var parentDirectory = Path.GetDirectoryName(path); - var extension = Path.GetExtension(path); + //#region Manage thumbnails - return GetFiles(parentDirectory) - .Where(x => x.StartsWith(path.TrimEnd(extension) + "_thumb") || x.StartsWith(path.TrimEnd(extension) + "_big-thumb")) - .ToList(); - } + //// note: this does not find 'custom' thumbnails? + //// will find _thumb and _big-thumb but NOT _custom? + //public IEnumerable GetThumbnails(string path) + //{ + // var parentDirectory = Path.GetDirectoryName(path); + // var extension = Path.GetExtension(path); - public void DeleteFile(string path, bool deleteThumbnails) - { - base.DeleteFile(path); + // return GetFiles(parentDirectory) + // .Where(x => x.StartsWith(path.TrimEnd(extension) + "_thumb") || x.StartsWith(path.TrimEnd(extension) + "_big-thumb")) + // .ToList(); + //} - if (deleteThumbnails == false) - return; + //public void DeleteFile(string path, bool deleteThumbnails) + //{ + // base.DeleteFile(path); - DeleteThumbnails(path); - } + // if (deleteThumbnails == false) + // return; - public void DeleteThumbnails(string path) - { - GetThumbnails(path) - .ForEach(x => base.DeleteFile(x)); - } + // DeleteThumbnails(path); + //} - public void CopyThumbnails(string sourcePath, string targetPath) - { - var targetPathBase = Path.GetDirectoryName(targetPath) ?? ""; - foreach (var sourceThumbPath in GetThumbnails(sourcePath)) - { - var sourceThumbFilename = Path.GetFileName(sourceThumbPath) ?? ""; - var targetThumbPath = Path.Combine(targetPathBase, sourceThumbFilename); - this.CopyFile(sourceThumbPath, targetThumbPath); - } - } + //public void DeleteThumbnails(string path) + //{ + // GetThumbnails(path) + // .ForEach(x => base.DeleteFile(x)); + //} - #endregion + //public void CopyThumbnails(string sourcePath, string targetPath) + //{ + // var targetPathBase = Path.GetDirectoryName(targetPath) ?? ""; + // foreach (var sourceThumbPath in GetThumbnails(sourcePath)) + // { + // var sourceThumbFilename = Path.GetFileName(sourceThumbPath) ?? ""; + // var targetThumbPath = Path.Combine(targetPathBase, sourceThumbFilename); + // this.CopyFile(sourceThumbPath, targetThumbPath); + // } + //} - #region GenerateThumbnails + //#endregion - public IEnumerable GenerateThumbnails(Image image, string filepath, string preValue) - { - if (string.IsNullOrWhiteSpace(preValue)) - return GenerateThumbnails(image, filepath); + //#region GenerateThumbnails - var additionalSizes = new List(); - var sep = preValue.Contains(",") ? "," : ";"; - var values = preValue.Split(new[] { sep }, StringSplitOptions.RemoveEmptyEntries); - foreach (var value in values) - { - int size; - if (int.TryParse(value, out size)) - additionalSizes.Add(size); - } + //public IEnumerable GenerateThumbnails(Image image, string filepath, string preValue) + //{ + // if (string.IsNullOrWhiteSpace(preValue)) + // return GenerateThumbnails(image, filepath); - return GenerateThumbnails(image, filepath, additionalSizes); - } + // var additionalSizes = new List(); + // var sep = preValue.Contains(",") ? "," : ";"; + // var values = preValue.Split(new[] { sep }, StringSplitOptions.RemoveEmptyEntries); + // foreach (var value in values) + // { + // int size; + // if (int.TryParse(value, out size)) + // additionalSizes.Add(size); + // } - public IEnumerable GenerateThumbnails(Image image, string filepath, IEnumerable additionalSizes = null) - { - var w = image.Width; - var h = image.Height; + // return GenerateThumbnails(image, filepath, additionalSizes); + //} - var sizes = additionalSizes == null ? DefaultSizes.Keys : DefaultSizes.Keys.Concat(additionalSizes); + //public IEnumerable GenerateThumbnails(Image image, string filepath, IEnumerable additionalSizes = null) + //{ + // var w = image.Width; + // var h = image.Height; - // start with default sizes, - // add additional sizes, - // filter out duplicates, - // filter out those that would be larger that the original image - // and create the thumbnail - return sizes - .Distinct() - .Where(x => w >= x && h >= x) - .Select(x => GenerateResized(image, filepath, DefaultSizes.ContainsKey(x) ? DefaultSizes[x] : "", x)) - .ToList(); // now - } + // var sizes = additionalSizes == null ? DefaultSizes.Keys : DefaultSizes.Keys.Concat(additionalSizes); - public IEnumerable GenerateThumbnails(Stream filestream, string filepath, PropertyType propertyType) - { - // get the original image from the original stream - if (filestream.CanSeek) filestream.Seek(0, 0); // fixme - what if we cannot seek? - using (var image = Image.FromStream(filestream)) - { - return GenerateThumbnails(image, filepath, propertyType); - } - } + // // start with default sizes, + // // add additional sizes, + // // filter out duplicates, + // // filter out those that would be larger that the original image + // // and create the thumbnail + // return sizes + // .Distinct() + // .Where(x => w >= x && h >= x) + // .Select(x => GenerateResized(image, filepath, DefaultSizes.ContainsKey(x) ? DefaultSizes[x] : "", x)) + // .ToList(); // now + //} - public IEnumerable GenerateThumbnails(Image image, string filepath, PropertyType propertyType) - { - // if the editor is an upload field, check for additional thumbnail sizes - // that can be defined in the prevalue for the property data type. otherwise, - // just use the default sizes. - var sizes = propertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias - ? DataTypeService - .GetPreValuesByDataTypeId(propertyType.DataTypeDefinitionId) - .FirstOrDefault() - : string.Empty; + //public IEnumerable GenerateThumbnails(Stream filestream, string filepath, PropertyType propertyType) + //{ + // // get the original image from the original stream + // if (filestream.CanSeek) filestream.Seek(0, 0); // fixme - what if we cannot seek? + // using (var image = Image.FromStream(filestream)) + // { + // return GenerateThumbnails(image, filepath, propertyType); + // } + //} - return GenerateThumbnails(image, filepath, sizes); - } + //public IEnumerable GenerateThumbnails(Image image, string filepath, PropertyType propertyType) + //{ + // // if the editor is an upload field, check for additional thumbnail sizes + // // that can be defined in the prevalue for the property data type. otherwise, + // // just use the default sizes. + // var sizes = propertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias + // ? DataTypeService + // .GetPreValuesByDataTypeId(propertyType.DataTypeDefinitionId) + // .FirstOrDefault() + // : string.Empty; - #endregion + // return GenerateThumbnails(image, filepath, sizes); + //} - #region GenerateResized - Generate at resized filepath derived from origin filepath + //#endregion - public ResizedImage GenerateResized(Image originImage, string originFilepath, string sizeName, int maxWidthHeight) - { - return GenerateResized(originImage, originFilepath, sizeName, maxWidthHeight, -1, -1); - } + //#region GenerateResized - Generate at resized filepath derived from origin filepath - public ResizedImage GenerateResized(Image originImage, string originFilepath, string sizeName, int fixedWidth, int fixedHeight) - { - return GenerateResized(originImage, originFilepath, sizeName, -1, fixedWidth, fixedHeight); - } + //public ResizedImage GenerateResized(Image originImage, string originFilepath, string sizeName, int maxWidthHeight) + //{ + // return GenerateResized(originImage, originFilepath, sizeName, maxWidthHeight, -1, -1); + //} - public ResizedImage GenerateResized(Image originImage, string originFilepath, string sizeName, int maxWidthHeight, int fixedWidth, int fixedHeight) - { - if (string.IsNullOrWhiteSpace(sizeName)) - sizeName = "UMBRACOSYSTHUMBNAIL"; - var extension = Path.GetExtension(originFilepath) ?? string.Empty; - var filebase = originFilepath.TrimEnd(extension); - var resizedFilepath = filebase + "_" + sizeName + extension; + //public ResizedImage GenerateResized(Image originImage, string originFilepath, string sizeName, int fixedWidth, int fixedHeight) + //{ + // return GenerateResized(originImage, originFilepath, sizeName, -1, fixedWidth, fixedHeight); + //} - return GenerateResizedAt(originImage, resizedFilepath, maxWidthHeight, fixedWidth, fixedHeight); - } + //public ResizedImage GenerateResized(Image originImage, string originFilepath, string sizeName, int maxWidthHeight, int fixedWidth, int fixedHeight) + //{ + // if (string.IsNullOrWhiteSpace(sizeName)) + // sizeName = "UMBRACOSYSTHUMBNAIL"; + // var extension = Path.GetExtension(originFilepath) ?? string.Empty; + // var filebase = originFilepath.TrimEnd(extension); + // var resizedFilepath = filebase + "_" + sizeName + extension; - #endregion + // return GenerateResizedAt(originImage, resizedFilepath, maxWidthHeight, fixedWidth, fixedHeight); + //} - #region GenerateResizedAt - Generate at specified resized filepath + //#endregion - public ResizedImage GenerateResizedAt(Image originImage, string resizedFilepath, int maxWidthHeight) - { - return GenerateResizedAt(originImage, resizedFilepath, maxWidthHeight, -1, -1); - } + //#region GenerateResizedAt - Generate at specified resized filepath - public ResizedImage GenerateResizedAt(Image originImage, int fixedWidth, int fixedHeight, string resizedFilepath) - { - return GenerateResizedAt(originImage, resizedFilepath, -1, fixedWidth, fixedHeight); - } + //public ResizedImage GenerateResizedAt(Image originImage, string resizedFilepath, int maxWidthHeight) + //{ + // return GenerateResizedAt(originImage, resizedFilepath, maxWidthHeight, -1, -1); + //} - public ResizedImage GenerateResizedAt(Image originImage, string resizedFilepath, int maxWidthHeight, int fixedWidth, int fixedHeight) - { - // target dimensions - int width, height; + //public ResizedImage GenerateResizedAt(Image originImage, int fixedWidth, int fixedHeight, string resizedFilepath) + //{ + // return GenerateResizedAt(originImage, resizedFilepath, -1, fixedWidth, fixedHeight); + //} - // if maxWidthHeight then get ratio - if (maxWidthHeight > 0) - { - var fx = (float)originImage.Size.Width / maxWidthHeight; - var fy = (float)originImage.Size.Height / maxWidthHeight; - var f = Math.Max(fx, fy); // fit in thumbnail size - width = (int)Math.Round(originImage.Size.Width / f); - height = (int)Math.Round(originImage.Size.Height / f); - if (width == 0) width = 1; - if (height == 0) height = 1; - } - else if (fixedWidth > 0 && fixedHeight > 0) - { - width = fixedWidth; - height = fixedHeight; - } - else - { - width = height = 1; - } + //public ResizedImage GenerateResizedAt(Image originImage, string resizedFilepath, int maxWidthHeight, int fixedWidth, int fixedHeight) + //{ + // // target dimensions + // int width, height; - // create new image with best quality settings - using (var bitmap = new Bitmap(width, height)) - using (var graphics = Graphics.FromImage(bitmap)) - { - // if the image size is rather large we cannot use the best quality interpolation mode - // because we'll get out of mem exceptions. So we detect how big the image is and use - // the mid quality interpolation mode when the image size exceeds our max limit. - graphics.InterpolationMode = originImage.Width > 5000 || originImage.Height > 5000 - ? InterpolationMode.Bilinear // mid quality - : InterpolationMode.HighQualityBicubic; // best quality + // // if maxWidthHeight then get ratio + // if (maxWidthHeight > 0) + // { + // var fx = (float)originImage.Size.Width / maxWidthHeight; + // var fy = (float)originImage.Size.Height / maxWidthHeight; + // var f = Math.Max(fx, fy); // fit in thumbnail size + // width = (int)Math.Round(originImage.Size.Width / f); + // height = (int)Math.Round(originImage.Size.Height / f); + // if (width == 0) width = 1; + // if (height == 0) height = 1; + // } + // else if (fixedWidth > 0 && fixedHeight > 0) + // { + // width = fixedWidth; + // height = fixedHeight; + // } + // else + // { + // width = height = 1; + // } - // everything else is best-quality - graphics.SmoothingMode = SmoothingMode.HighQuality; - graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; - graphics.CompositingQuality = CompositingQuality.HighQuality; + // // create new image with best quality settings + // using (var bitmap = new Bitmap(width, height)) + // using (var graphics = Graphics.FromImage(bitmap)) + // { + // // if the image size is rather large we cannot use the best quality interpolation mode + // // because we'll get out of mem exceptions. So we detect how big the image is and use + // // the mid quality interpolation mode when the image size exceeds our max limit. + // graphics.InterpolationMode = originImage.Width > 5000 || originImage.Height > 5000 + // ? InterpolationMode.Bilinear // mid quality + // : InterpolationMode.HighQualityBicubic; // best quality - // copy the old image to the new and resize - var rect = new Rectangle(0, 0, width, height); - graphics.DrawImage(originImage, rect, 0, 0, originImage.Width, originImage.Height, GraphicsUnit.Pixel); + // // everything else is best-quality + // graphics.SmoothingMode = SmoothingMode.HighQuality; + // graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; + // graphics.CompositingQuality = CompositingQuality.HighQuality; - // copy metadata - // fixme - er... no? + // // copy the old image to the new and resize + // var rect = new Rectangle(0, 0, width, height); + // graphics.DrawImage(originImage, rect, 0, 0, originImage.Width, originImage.Height, GraphicsUnit.Pixel); - // get an encoder - based upon the file type - var extension = (Path.GetExtension(resizedFilepath) ?? "").TrimStart('.').ToLowerInvariant(); - var encoders = ImageCodecInfo.GetImageEncoders(); - ImageCodecInfo encoder; - switch (extension) - { - case "png": - encoder = encoders.Single(t => t.MimeType.Equals("image/png")); - break; - case "gif": - encoder = encoders.Single(t => t.MimeType.Equals("image/gif")); - break; - case "tif": - case "tiff": - encoder = encoders.Single(t => t.MimeType.Equals("image/tiff")); - break; - case "bmp": - encoder = encoders.Single(t => t.MimeType.Equals("image/bmp")); - break; - // TODO: this is dirty, defaulting to jpg but the return value of this thing is used all over the - // place so left it here, but it needs to not set a codec if it doesn't know which one to pick - // Note: when fixing this: both .jpg and .jpeg should be handled as extensions - default: - encoder = encoders.Single(t => t.MimeType.Equals("image/jpeg")); - break; - } + // // copy metadata + // // fixme - er... no? - // set compresion ratio to 90% - var encoderParams = new EncoderParameters(); - encoderParams.Param[0] = new EncoderParameter(Encoder.Quality, 90L); + // // get an encoder - based upon the file type + // var extension = (Path.GetExtension(resizedFilepath) ?? "").TrimStart('.').ToLowerInvariant(); + // var encoders = ImageCodecInfo.GetImageEncoders(); + // ImageCodecInfo encoder; + // switch (extension) + // { + // case "png": + // encoder = encoders.Single(t => t.MimeType.Equals("image/png")); + // break; + // case "gif": + // encoder = encoders.Single(t => t.MimeType.Equals("image/gif")); + // break; + // case "tif": + // case "tiff": + // encoder = encoders.Single(t => t.MimeType.Equals("image/tiff")); + // break; + // case "bmp": + // encoder = encoders.Single(t => t.MimeType.Equals("image/bmp")); + // break; + // // TODO: this is dirty, defaulting to jpg but the return value of this thing is used all over the + // // place so left it here, but it needs to not set a codec if it doesn't know which one to pick + // // Note: when fixing this: both .jpg and .jpeg should be handled as extensions + // default: + // encoder = encoders.Single(t => t.MimeType.Equals("image/jpeg")); + // break; + // } - // save the new image - using (var stream = new MemoryStream()) - { - bitmap.Save(stream, encoder, encoderParams); - stream.Seek(0, 0); - if (resizedFilepath.Contains("UMBRACOSYSTHUMBNAIL")) - { - var filepath = resizedFilepath.Replace("UMBRACOSYSTHUMBNAIL", maxWidthHeight.ToInvariantString()); - AddFile(filepath, stream); - if (extension != "jpg") - { - filepath = filepath.TrimEnd(extension) + "jpg"; - stream.Seek(0, 0); - AddFile(filepath, stream); - } - // TODO: Remove this, this is ONLY here for backwards compatibility but it is essentially completely unusable see U4-5385 - stream.Seek(0, 0); - resizedFilepath = resizedFilepath.Replace("UMBRACOSYSTHUMBNAIL", width + "x" + height); - } + // // set compresion ratio to 90% + // var encoderParams = new EncoderParameters(); + // encoderParams.Param[0] = new EncoderParameter(Encoder.Quality, 90L); - AddFile(resizedFilepath, stream); - } + // // save the new image + // using (var stream = new MemoryStream()) + // { + // bitmap.Save(stream, encoder, encoderParams); + // stream.Seek(0, 0); + // if (resizedFilepath.Contains("UMBRACOSYSTHUMBNAIL")) + // { + // var filepath = resizedFilepath.Replace("UMBRACOSYSTHUMBNAIL", maxWidthHeight.ToInvariantString()); + // AddFile(filepath, stream); + // if (extension != "jpg") + // { + // filepath = filepath.TrimEnd(extension) + "jpg"; + // stream.Seek(0, 0); + // AddFile(filepath, stream); + // } + // // TODO: Remove this, this is ONLY here for backwards compatibility but it is essentially completely unusable see U4-5385 + // stream.Seek(0, 0); + // resizedFilepath = resizedFilepath.Replace("UMBRACOSYSTHUMBNAIL", width + "x" + height); + // } - return new ResizedImage(resizedFilepath, width, height); - } - } + // AddFile(resizedFilepath, stream); + // } - #endregion + // return new ResizedImage(resizedFilepath, width, height); + // } + //} - #region Inner classes + //#endregion - public class ResizedImage - { - public ResizedImage() - { } + //#region Inner classes - public ResizedImage(string filepath, int width, int height) - { - Filepath = filepath; - Width = width; - Height = height; - } + //public class ResizedImage + //{ + // public ResizedImage() + // { } - public string Filepath { get; set; } - public int Width { get; set; } - public int Height { get; set; } - } + // public ResizedImage(string filepath, int width, int height) + // { + // Filepath = filepath; + // Width = width; + // Height = height; + // } - #endregion + // public string Filepath { get; set; } + // public int Width { get; set; } + // public int Height { get; set; } + //} + + //#endregion } } diff --git a/src/Umbraco.Core/IO/PhysicalFileSystem.cs b/src/Umbraco.Core/IO/PhysicalFileSystem.cs index 77c461693d..77a2baebdb 100644 --- a/src/Umbraco.Core/IO/PhysicalFileSystem.cs +++ b/src/Umbraco.Core/IO/PhysicalFileSystem.cs @@ -361,6 +361,29 @@ namespace Umbraco.Core.IO return file.Exists ? file.Length : -1; } + public bool CanAddPhysical => true; + + public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false) + { + var fullPath = GetFullPath(path); + + if (File.Exists(fullPath)) + { + if (overrideIfExists == false) + throw new InvalidOperationException($"A file at path '{path}' already exists"); + File.Delete(fullPath); + } + + var directory = Path.GetDirectoryName(fullPath); + if (directory == null) throw new InvalidOperationException("Could not get directory."); + Directory.CreateDirectory(directory); // ensure it exists + + if (copy) + File.Copy(physicalPath, fullPath); + else + File.Move(physicalPath, fullPath); + } + #region Helper Methods protected virtual void EnsureDirectory(string path) diff --git a/src/Umbraco.Core/IO/ShadowFileSystem.cs b/src/Umbraco.Core/IO/ShadowFileSystem.cs index b11b8cba96..84ff1b428b 100644 --- a/src/Umbraco.Core/IO/ShadowFileSystem.cs +++ b/src/Umbraco.Core/IO/ShadowFileSystem.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; namespace Umbraco.Core.IO { @@ -30,8 +31,15 @@ namespace Umbraco.Core.IO { try { - using (var stream = _sfs.OpenFile(kvp.Key)) - _fs.AddFile(kvp.Key, stream, true); + if (_fs.CanAddPhysical) + { + _fs.AddFile(kvp.Key, _sfs.GetFullPath(kvp.Key)); // overwrite, move + } + else + { + using (var stream = _sfs.OpenFile(kvp.Key)) + _fs.AddFile(kvp.Key, stream, true); + } } catch (Exception e) { @@ -198,19 +206,20 @@ namespace Umbraco.Core.IO public IEnumerable GetFiles(string path) { - var normPath = NormPath(path); - var shadows = Nodes.Where(kvp => IsChild(normPath, kvp.Key)).ToArray(); - var files = _fs.GetFiles(path); - return files - .Except(shadows.Where(kvp => (kvp.Value.IsFile && kvp.Value.IsDelete) || kvp.Value.IsDir) - .Select(kvp => kvp.Key)) - .Union(shadows.Where(kvp => kvp.Value.IsFile && kvp.Value.IsExist).Select(kvp => kvp.Key)) - .Distinct(); + return GetFiles(path, null); } public IEnumerable GetFiles(string path, string filter) { - return _fs.GetFiles(path, filter); + var normPath = NormPath(path); + var shadows = Nodes.Where(kvp => IsChild(normPath, kvp.Key)).ToArray(); + var files = filter != null ? _fs.GetFiles(path, filter) : _fs.GetFiles(path); + var wildcard = filter == null ? null : new WildcardExpression(filter); + return files + .Except(shadows.Where(kvp => (kvp.Value.IsFile && kvp.Value.IsDelete) || kvp.Value.IsDir) + .Select(kvp => kvp.Key)) + .Union(shadows.Where(kvp => kvp.Value.IsFile && kvp.Value.IsExist && (wildcard == null || wildcard.IsMatch(kvp.Key))).Select(kvp => kvp.Key)) + .Distinct(); } public Stream OpenFile(string path) @@ -242,6 +251,9 @@ namespace Umbraco.Core.IO public string GetFullPath(string path) { + ShadowNode sf; + if (Nodes.TryGetValue(NormPath(path), out sf)) + return sf.IsDir || sf.IsDelete ? null : _sfs.GetFullPath(path); return _fs.GetFullPath(path); } @@ -275,5 +287,96 @@ namespace Umbraco.Core.IO if (sf.IsDelete || sf.IsDir) throw new InvalidOperationException("Invalid path."); return _sfs.GetSize(path); } + + public bool CanAddPhysical { get { return true; } } + + public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false) + { + ShadowNode sf; + var normPath = NormPath(path); + if (Nodes.TryGetValue(normPath, out sf) && sf.IsExist && (sf.IsDir || overrideIfExists == false)) + throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path)); + + var parts = normPath.Split('/'); + for (var i = 0; i < parts.Length - 1; i++) + { + var dirPath = string.Join("/", parts.Take(i + 1)); + ShadowNode sd; + if (Nodes.TryGetValue(dirPath, out sd)) + { + if (sd.IsFile) throw new InvalidOperationException("Invalid path."); + if (sd.IsDelete) Nodes[dirPath] = new ShadowNode(false, true); + } + else + { + if (_fs.DirectoryExists(dirPath)) continue; + if (_fs.FileExists(dirPath)) throw new InvalidOperationException("Invalid path."); + Nodes[dirPath] = new ShadowNode(false, true); + } + } + + _sfs.AddFile(path, physicalPath, overrideIfExists, copy); + Nodes[normPath] = new ShadowNode(false, false); + } + + // copied from System.Web.Util.Wildcard internal + internal class WildcardExpression + { + private readonly string _pattern; + private readonly bool _caseInsensitive; + private Regex _regex; + + private static Regex metaRegex = new Regex("[\\+\\{\\\\\\[\\|\\(\\)\\.\\^\\$]"); + private static Regex questRegex = new Regex("\\?"); + private static Regex starRegex = new Regex("\\*"); + private static Regex commaRegex = new Regex(","); + private static Regex slashRegex = new Regex("(?=/)"); + private static Regex backslashRegex = new Regex("(?=[\\\\:])"); + + public WildcardExpression(string pattern, bool caseInsensitive = true) + { + _pattern = pattern; + _caseInsensitive = caseInsensitive; + } + + private void EnsureRegex(string pattern) + { + if (_regex != null) return; + + var options = RegexOptions.None; + + // match right-to-left (for speed) if the pattern starts with a * + + if (pattern.Length > 0 && pattern[0] == '*') + options = RegexOptions.RightToLeft | RegexOptions.Singleline; + else + options = RegexOptions.Singleline; + + // case insensitivity + + if (_caseInsensitive) + options |= RegexOptions.IgnoreCase | RegexOptions.CultureInvariant; + + // Remove regex metacharacters + + pattern = metaRegex.Replace(pattern, "\\$0"); + + // Replace wildcard metacharacters with regex codes + + pattern = questRegex.Replace(pattern, "."); + pattern = starRegex.Replace(pattern, ".*"); + pattern = commaRegex.Replace(pattern, "\\z|\\A"); + + // anchor the pattern at beginning and end, and return the regex + + _regex = new Regex("\\A" + pattern + "\\z", options); + } + + public bool IsMatch(string input) + { + EnsureRegex(_pattern); + return _regex.IsMatch(input); + } + } } } diff --git a/src/Umbraco.Core/IO/ShadowFileSystems.cs b/src/Umbraco.Core/IO/ShadowFileSystems.cs new file mode 100644 index 0000000000..1bfa2ebc18 --- /dev/null +++ b/src/Umbraco.Core/IO/ShadowFileSystems.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Logging; + +namespace Umbraco.Core.IO +{ + internal class ShadowFileSystems : ICompletable + { + // note: taking a reference to the _manager instead of using manager.Current + // to avoid using Current everywhere but really, we support only 1 scope at + // a time, not multiple scopes in case of multiple managers (not supported) + + // fixme - why are we managing logical call context here? should be bound + // to the current scope, always => REFACTOR! but there should be something in + // place (static?) to ensure we only have one concurrent shadow FS? + + private const string ItemKey = "Umbraco.Core.IO.ShadowFileSystems"; + + private static readonly object Locker = new object(); + private static Guid _currentId = Guid.Empty; + + private readonly Guid _id; + private readonly ShadowWrapper[] _wrappers; + private readonly ILogger _logger; + + private bool _completed; + + //static ShadowFileSystems() + //{ + // SafeCallContext.Register( + // () => + // { + // var scope = CallContext.LogicalGetData(ItemKey); + // CallContext.FreeNamedDataSlot(ItemKey); + // return scope; + // }, + // o => + // { + // if (CallContext.LogicalGetData(ItemKey) != null) throw new InvalidOperationException(); + // if (o != null) CallContext.LogicalSetData(ItemKey, o); + // }); + //} + + public ShadowFileSystems(Guid id, ShadowWrapper[] wrappers, ILogger logger) + { + lock (Locker) + { + if (_currentId != Guid.Empty) + throw new InvalidOperationException("Already shadowing."); + _currentId = id; + } + + _logger = logger; + _logger.Debug("Shadow " + id + "."); + _id = id; + + _wrappers = wrappers; + foreach (var wrapper in _wrappers) + wrapper.Shadow(id); + } + + // fixme - remove + //// internal for tests + FileSystems + //// do NOT use otherwise + //internal static ShadowFileSystems CreateScope(Guid id, ShadowWrapper[] wrappers, ILogger logger) + //{ + // lock (Locker) + // { + // if (CallContext.LogicalGetData(ItemKey) != null) throw new InvalidOperationException("Already shadowing."); + // CallContext.LogicalSetData(ItemKey, ItemKey); // value does not matter + // } + // return new ShadowFileSystems(id, wrappers, logger); + //} + + //internal static bool InScope => NoScope == false; + + //internal static bool NoScope => CallContext.LogicalGetData(ItemKey) == null; + + public void Complete() + { + _completed = true; + //lock (Locker) + //{ + // _logger.Debug("UnShadow " + _id + " (complete)."); + + // var exceptions = new List(); + // foreach (var wrapper in _wrappers) + // { + // try + // { + // // this may throw an AggregateException if some of the changes could not be applied + // wrapper.UnShadow(true); + // } + // catch (AggregateException ae) + // { + // exceptions.Add(ae); + // } + // } + + // if (exceptions.Count > 0) + // throw new AggregateException("Failed to apply all changes (see exceptions).", exceptions); + + // // last, & *only* if successful (otherwise we'll unshadow & cleanup as best as we can) + // CallContext.FreeNamedDataSlot(ItemKey); + //} + } + + public void Dispose() + { + lock (Locker) + { + _logger.Debug("UnShadow " + _id + " (" + (_completed ? "complete" : "abort") + ")."); + + var exceptions = new List(); + foreach (var wrapper in _wrappers) + { + try + { + // this may throw an AggregateException if some of the changes could not be applied + wrapper.UnShadow(_completed); + } + catch (AggregateException ae) + { + exceptions.Add(ae); + } + } + + _currentId = Guid.Empty; + + if (exceptions.Count > 0) + throw new AggregateException(_completed ? "Failed to apply all changes (see exceptions)." : "Failed to abort (see exceptions).", exceptions); + + //if (CallContext.LogicalGetData(ItemKey) == null) return; + + //try + //{ + // _logger.Debug("UnShadow " + _id + " (abort)"); + // foreach (var wrapper in _wrappers) + // wrapper.UnShadow(false); // should not throw + //} + //finally + //{ + // // last, & always + // CallContext.FreeNamedDataSlot(ItemKey); + //} + } + } + + // for tests + internal static void ResetId() + { + _currentId = Guid.Empty; + } + } +} diff --git a/src/Umbraco.Core/IO/ShadowFileSystemsScope.cs b/src/Umbraco.Core/IO/ShadowFileSystemsScope.cs deleted file mode 100644 index bc59317bb2..0000000000 --- a/src/Umbraco.Core/IO/ShadowFileSystemsScope.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.Remoting.Messaging; -using Umbraco.Core.Logging; - -namespace Umbraco.Core.IO -{ - internal class ShadowFileSystemsScope : ICompletable - { - // note: taking a reference to the _manager instead of using manager.Current - // to avoid using Current everywhere but really, we support only 1 scope at - // a time, not multiple scopes in case of multiple managers (not supported) - - private const string ItemKey = "Umbraco.Core.IO.ShadowFileSystemsScope"; - private static readonly object Locker = new object(); - private readonly Guid _id; - private readonly ShadowWrapper[] _wrappers; - private readonly ILogger _logger; - - static ShadowFileSystemsScope() - { - SafeCallContext.Register( - () => - { - var scope = CallContext.LogicalGetData(ItemKey); - CallContext.FreeNamedDataSlot(ItemKey); - return scope; - }, - o => - { - if (CallContext.LogicalGetData(ItemKey) != null) throw new InvalidOperationException(); - if (o != null) CallContext.LogicalSetData(ItemKey, o); - }); - } - - private ShadowFileSystemsScope(Guid id, ShadowWrapper[] wrappers, ILogger logger) - { - _logger = logger; - _logger.Debug("Shadow " + id + "."); - _id = id; - _wrappers = wrappers; - foreach (var wrapper in _wrappers) - wrapper.Shadow(id); - } - - // internal for tests + FileSystems - // do NOT use otherwise - internal static ShadowFileSystemsScope CreateScope(Guid id, ShadowWrapper[] wrappers, ILogger logger) - { - lock (Locker) - { - if (CallContext.LogicalGetData(ItemKey) != null) throw new InvalidOperationException("Already shadowing."); - CallContext.LogicalSetData(ItemKey, ItemKey); // value does not matter - } - return new ShadowFileSystemsScope(id, wrappers, logger); - } - - internal static bool InScope => NoScope == false; - - internal static bool NoScope => CallContext.LogicalGetData(ItemKey) == null; - - public void Complete() - { - lock (Locker) - { - _logger.Debug("UnShadow " + _id + " (complete)."); - - var exceptions = new List(); - foreach (var wrapper in _wrappers) - { - try - { - // this may throw an AggregateException if some of the changes could not be applied - wrapper.UnShadow(true); - } - catch (AggregateException ae) - { - exceptions.Add(ae); - } - } - - if (exceptions.Count > 0) - throw new AggregateException("Failed to apply all changes (see exceptions).", exceptions); - - // last, & *only* if successful (otherwise we'll unshadow & cleanup as best as we can) - CallContext.FreeNamedDataSlot(ItemKey); - } - } - - public void Dispose() - { - lock (Locker) - { - if (CallContext.LogicalGetData(ItemKey) == null) return; - - try - { - _logger.Debug("UnShadow " + _id + " (abort)"); - foreach (var wrapper in _wrappers) - wrapper.UnShadow(false); // should not throw - } - finally - { - // last, & always - CallContext.FreeNamedDataSlot(ItemKey); - } - } - } - } -} diff --git a/src/Umbraco.Core/IO/ShadowWrapper.cs b/src/Umbraco.Core/IO/ShadowWrapper.cs index 64310622ca..b53355211f 100644 --- a/src/Umbraco.Core/IO/ShadowWrapper.cs +++ b/src/Umbraco.Core/IO/ShadowWrapper.cs @@ -2,20 +2,23 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using Umbraco.Core.Scoping; namespace Umbraco.Core.IO { internal class ShadowWrapper : IFileSystem { + private readonly IScopeProviderInternal _scopeProvider; private readonly IFileSystem _innerFileSystem; private readonly string _shadowPath; private ShadowFileSystem _shadowFileSystem; private string _shadowDir; - public ShadowWrapper(IFileSystem innerFileSystem, string shadowPath) + public ShadowWrapper(IFileSystem innerFileSystem, string shadowPath, IScopeProviderInternal scopeProvider) { _innerFileSystem = innerFileSystem; _shadowPath = shadowPath; + _scopeProvider = scopeProvider; } internal void Shadow(Guid id) @@ -24,7 +27,7 @@ namespace Umbraco.Core.IO // on ShadowFileSystemsScope.None - and if None is false then we should be running // in a single thread anyways - var virt = "~/App_Data/Shadow/" + id + "/" + _shadowPath; + var virt = "~/App_Data/TEMP/ShadowFs/" + id + "/" + _shadowPath; _shadowDir = IOHelper.MapPath(virt); Directory.CreateDirectory(_shadowDir); var tempfs = new PhysicalFileSystem(virt); @@ -62,7 +65,19 @@ namespace Umbraco.Core.IO private IFileSystem FileSystem { - get { return ShadowFileSystemsScope.NoScope ? _innerFileSystem : _shadowFileSystem; } + get + { + var isScoped = _scopeProvider?.AmbientScope != null && _scopeProvider.AmbientScope.ScopedFileSystems; + + // if the filesystem is created *after* shadowing starts, it won't be shadowing + // better not ignore that situation and raised a meaningful (?) exception + if (isScoped && _shadowFileSystem == null) + throw new Exception("The filesystems are shadowing, but this filesystem is not."); + + return isScoped + ? _shadowFileSystem + : _innerFileSystem; + } } public IEnumerable GetDirectories(string path) @@ -149,5 +164,12 @@ namespace Umbraco.Core.IO { return FileSystem.GetSize(path); } + + public bool CanAddPhysical => FileSystem.CanAddPhysical; + + public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false) + { + FileSystem.AddFile(path, physicalPath, overrideIfExists, copy); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/IO/SystemFiles.cs b/src/Umbraco.Core/IO/SystemFiles.cs index c4fc2e0dee..85f0a1f42c 100644 --- a/src/Umbraco.Core/IO/SystemFiles.cs +++ b/src/Umbraco.Core/IO/SystemFiles.cs @@ -1,9 +1,5 @@ using System; -using System.Collections.Generic; -using System.Configuration; using System.IO; -using System.Linq; -using System.Text; using System.Web; using Umbraco.Core.Configuration; @@ -11,57 +7,39 @@ namespace Umbraco.Core.IO { public class SystemFiles { - public static string CreateUiXml - { - get - { - return SystemDirectories.Umbraco + "/config/create/UI.xml"; - } - } + public static string CreateUiXml => SystemDirectories.Umbraco + "/config/create/UI.xml"; - public static string TinyMceConfig - { - get - { - return SystemDirectories.Config + "/tinyMceConfig.config"; - } - } + public static string TinyMceConfig => SystemDirectories.Config + "/tinyMceConfig.config"; - public static string DashboardConfig - { - get - { - return SystemDirectories.Config + "/dashboard.config"; - } - } + public static string DashboardConfig => SystemDirectories.Config + "/dashboard.config"; - public static string NotFoundhandlersConfig - { - get - { - return SystemDirectories.Config + "/404handlers.config"; - } - } + public static string NotFoundhandlersConfig => SystemDirectories.Config + "/404handlers.config"; - public static string FeedProxyConfig - { - get - { - return string.Concat(SystemDirectories.Config, "/feedProxy.config"); - } - } + public static string FeedProxyConfig => string.Concat(SystemDirectories.Config, "/feedProxy.config"); + // fixme - kill public static string ContentCacheXml { get { - if (GlobalSettings.ContentCacheXmlStoredInCodeGen && SystemUtilities.GetCurrentTrustLevel() == AspNetHostingPermissionLevel.Unrestricted) + switch (GlobalSettings.ContentCacheXmlStorageLocation) { - return Path.Combine(HttpRuntime.CodegenDir, @"UmbracoData\umbraco.config"); + case ContentXmlStorage.AspNetTemp: + return Path.Combine(HttpRuntime.CodegenDir, @"UmbracoData\umbraco.config"); + case ContentXmlStorage.EnvironmentTemp: + var appDomainHash = HttpRuntime.AppDomainAppId.ToSHA1(); + var cachePath = Path.Combine(Environment.ExpandEnvironmentVariables("%temp%"), "UmbracoXml", + //include the appdomain hash is just a safety check, for example if a website is moved from worker A to worker B and then back + // to worker A again, in theory the %temp% folder should already be empty but we really want to make sure that its not + // utilizing an old path + appDomainHash); + return Path.Combine(cachePath, "umbraco.config"); + case ContentXmlStorage.Default: + return IOHelper.ReturnPath("umbracoContentXML", "~/App_Data/umbraco.config"); + default: + throw new ArgumentOutOfRangeException(); } - return IOHelper.ReturnPath("umbracoContentXML", "~/App_Data/umbraco.config"); } - } - + } } } diff --git a/src/Umbraco.Core/Logging/AsynchronousRollingFileAppender.cs b/src/Umbraco.Core/Logging/AsynchronousRollingFileAppender.cs index c226bd03c8..cd62008cd4 100644 --- a/src/Umbraco.Core/Logging/AsynchronousRollingFileAppender.cs +++ b/src/Umbraco.Core/Logging/AsynchronousRollingFileAppender.cs @@ -16,7 +16,7 @@ namespace Umbraco.Core.Logging /// This is an old/deprecated logger and has been superceded by ParallelForwardingAppender which is included in Umbraco and /// also by AsyncForwardingAppender in the Log4Net.Async library. /// - [Obsolete("This is superceded by the ParallelForwardingAppender, this will be removed in v8")] + [Obsolete("This is superceded by the ParallelForwardingAppender, this will be removed in v8, do not use this")] [EditorBrowsable(EditorBrowsableState.Never)] public class AsynchronousRollingFileAppender : RollingFileAppender { diff --git a/src/Umbraco.Core/Logging/DebugDiagnosticsLogger.cs b/src/Umbraco.Core/Logging/DebugDiagnosticsLogger.cs index 1c65ac26e1..4e7d1544df 100644 --- a/src/Umbraco.Core/Logging/DebugDiagnosticsLogger.cs +++ b/src/Umbraco.Core/Logging/DebugDiagnosticsLogger.cs @@ -15,9 +15,9 @@ namespace Umbraco.Core.Logging } /// - public void Warn(Type reporting, string message) + public void Warn(Type reporting, string format) { - System.Diagnostics.Debug.WriteLine(message, reporting.FullName); + System.Diagnostics.Debug.WriteLine(format, reporting.FullName); } /// diff --git a/src/Umbraco.Core/Logging/ILogger.cs b/src/Umbraco.Core/Logging/ILogger.cs index 2eec5dbd14..740c1c1317 100644 --- a/src/Umbraco.Core/Logging/ILogger.cs +++ b/src/Umbraco.Core/Logging/ILogger.cs @@ -21,8 +21,8 @@ namespace Umbraco.Core.Logging /// Logs a warning message. /// /// The reporting type. - /// A message. - void Warn(Type reporting, string message); + /// A message. + void Warn(Type reporting, string format); /// /// Logs a warning message. diff --git a/src/Umbraco.Core/Logging/ImageProcessorLogger.cs b/src/Umbraco.Core/Logging/ImageProcessorLogger.cs index b1551395b4..0c156fce59 100644 --- a/src/Umbraco.Core/Logging/ImageProcessorLogger.cs +++ b/src/Umbraco.Core/Logging/ImageProcessorLogger.cs @@ -1,11 +1,10 @@ -using Umbraco.Core.DI; +using System; +using System.Runtime.CompilerServices; +using ImageProcessor.Common.Exceptions; +using Umbraco.Core.DI; namespace Umbraco.Core.Logging -{ - using System; - using System.Runtime.CompilerServices; - - using ImageProcessor.Common.Exceptions; +{ /// /// A logger for explicitly logging ImageProcessor exceptions. diff --git a/src/Umbraco.Core/Logging/Logger.cs b/src/Umbraco.Core/Logging/Logger.cs index c545de5986..333ada047c 100644 --- a/src/Umbraco.Core/Logging/Logger.cs +++ b/src/Umbraco.Core/Logging/Logger.cs @@ -3,8 +3,12 @@ using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; +using System.Reflection; +using System.Threading; using log4net; using log4net.Config; +using Umbraco.Core.Configuration; +using Umbraco.Core.Diagnostics; using log4net.Util; namespace Umbraco.Core.Logging @@ -45,15 +49,70 @@ namespace Umbraco.Core.Logging public void Error(Type reporting, string message, Exception exception = null) { var logger = LogManager.GetLogger(reporting); - logger?.Error(message, exception); + if (logger == null) return; + + var dump = false; + + if (IsTimeoutThreadAbortException(exception)) + { + message += "\r\nThe thread has been aborted, because the request has timed out."; + + // dump if configured, or if stacktrace contains Monitor.ReliableEnter + dump = UmbracoConfig.For.CoreDebug().DumpOnTimeoutThreadAbort || IsMonitorEnterThreadAbortException(exception); + + // dump if it is ok to dump (might have a cap on number of dump...) + dump &= MiniDump.OkToDump(); + } + + if (dump) + { + try + { + var dumped = MiniDump.Dump(withException: true); + message += dumped + ? "\r\nA minidump was created in App_Data/MiniDump" + : "\r\nFailed to create a minidump"; + } + catch (Exception e) + { + message += string.Format("\r\nFailed to create a minidump ({0}: {1})", e.GetType().FullName, e.Message); + } + } + + logger.Error(message, exception); } + private static bool IsMonitorEnterThreadAbortException(Exception exception) + { + var abort = exception as ThreadAbortException; + if (abort == null) return false; + + var stacktrace = abort.StackTrace; + return stacktrace.Contains("System.Threading.Monitor.ReliableEnter"); + } + + private static bool IsTimeoutThreadAbortException(Exception exception) + { + var abort = exception as ThreadAbortException; + if (abort == null) 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); + } + /// - public void Warn(Type reporting, string message) + public void Warn(Type reporting, string format) { var logger = LogManager.GetLogger(reporting); if (logger == null || logger.IsWarnEnabled == false) return; - logger.Warn(message); + logger.Warn(format); } /// diff --git a/src/Umbraco.Core/Logging/ParallelForwardingAppender.cs b/src/Umbraco.Core/Logging/ParallelForwardingAppender.cs index 48bb3ec710..f582b84f67 100644 --- a/src/Umbraco.Core/Logging/ParallelForwardingAppender.cs +++ b/src/Umbraco.Core/Logging/ParallelForwardingAppender.cs @@ -1,313 +1,21 @@ -using log4net.Core; -using log4net.Util; -using System; -using System.Collections.Concurrent; -using System.Threading; -using System.Threading.Tasks; +// fixme - remove this file +//using log4net.Core; +//using log4net.Util; +//using System; +//using System.Collections.Concurrent; +//using System.Threading; +//using System.Threading.Tasks; -namespace Umbraco.Core.Logging -{ - /// - /// An asynchronous appender based on - /// - /// - /// Borrowed from https://github.com/cjbhaines/Log4Net.Async - will reference Nuget packages directly in v8 - /// - public class ParallelForwardingAppender : AsyncForwardingAppenderBase, IDisposable - { - #region Private Members - - private const int DefaultBufferSize = 1000; - private BlockingCollection _loggingEvents; - private CancellationTokenSource _loggingCancelationTokenSource; - private CancellationToken _loggingCancelationToken; - private Task _loggingTask; - private Double _shutdownFlushTimeout = 2; - private TimeSpan _shutdownFlushTimespan = TimeSpan.FromSeconds(2); - private static readonly Type ThisType = typeof(ParallelForwardingAppender); - private volatile bool shutDownRequested; - private int? bufferSize = DefaultBufferSize; - - #endregion Private Members - - #region Properties - - /// - /// Gets or sets the number of LoggingEvents that will be buffered. Set to null for unlimited. - /// - public override int? BufferSize - { - get { return bufferSize; } - set { bufferSize = value; } - } - - public int BufferEntryCount - { - get - { - if (_loggingEvents == null) return 0; - return _loggingEvents.Count; - } - } - - /// - /// Gets or sets the time period in which the system will wait for appenders to flush before canceling the background task. - /// - public Double ShutdownFlushTimeout - { - get - { - return _shutdownFlushTimeout; - } - set - { - _shutdownFlushTimeout = value; - } - } - - protected override string InternalLoggerName - { - get - { - { - return "ParallelForwardingAppender"; - } - } - } - - #endregion Properties - - #region Startup - - public override void ActivateOptions() - { - base.ActivateOptions(); - _shutdownFlushTimespan = TimeSpan.FromSeconds(_shutdownFlushTimeout); - StartForwarding(); - } - - private void StartForwarding() - { - if (shutDownRequested) - { - return; - } - //Create a collection which will block the thread and wait for new entries - //if the collection is empty - if (BufferSize.HasValue && BufferSize > 0) - { - _loggingEvents = new BlockingCollection(BufferSize.Value); - } - else - { - //No limit on the number of events. - _loggingEvents = new BlockingCollection(); - } - //The cancellation token is used to cancel a running task gracefully. - _loggingCancelationTokenSource = new CancellationTokenSource(); - _loggingCancelationToken = _loggingCancelationTokenSource.Token; - _loggingTask = new Task(SubscriberLoop, _loggingCancelationToken); - _loggingTask.Start(); - } - - #endregion Startup - - #region Shutdown - - private void CompleteSubscriberTask() - { - shutDownRequested = true; - if (_loggingEvents == null || _loggingEvents.IsAddingCompleted) - { - return; - } - //Don't allow more entries to be added. - _loggingEvents.CompleteAdding(); - //Allow some time to flush - Thread.Sleep(_shutdownFlushTimespan); - if (!_loggingTask.IsCompleted && !_loggingCancelationToken.IsCancellationRequested) - { - _loggingCancelationTokenSource.Cancel(); - //Wait here so that the error logging messages do not get into a random order. - //Don't pass the cancellation token because we are not interested - //in catching the OperationCanceledException that results. - _loggingTask.Wait(); - } - if (!_loggingEvents.IsCompleted) - { - ForwardInternalError("The buffer was not able to be flushed before timeout occurred.", null, ThisType); - } - } - - protected override void OnClose() - { - CompleteSubscriberTask(); - base.OnClose(); - } - - #endregion Shutdown - - #region Appending - - protected override void Append(LoggingEvent loggingEvent) - { - if (_loggingEvents == null || _loggingEvents.IsAddingCompleted || loggingEvent == null) - { - return; - } - - loggingEvent.Fix = Fix; - //In the case where blocking on a full collection, and the task is subsequently completed, the cancellation token - //will prevent the entry from attempting to add to the completed collection which would result in an exception. - _loggingEvents.Add(new LoggingEventContext(loggingEvent, HttpContext), _loggingCancelationToken); - } - - protected override void Append(LoggingEvent[] loggingEvents) - { - if (_loggingEvents == null || _loggingEvents.IsAddingCompleted || loggingEvents == null) - { - return; - } - - foreach (var loggingEvent in loggingEvents) - { - Append(loggingEvent); - } - } - - #endregion Appending - - #region Forwarding - - /// - /// Iterates over a BlockingCollection containing LoggingEvents. - /// - private void SubscriberLoop() - { - Thread.CurrentThread.Name = String.Format("{0} ParallelForwardingAppender Subscriber Task", Name); - //The task will continue in a blocking loop until - //the queue is marked as adding completed, or the task is canceled. - try - { - //This call blocks until an item is available or until adding is completed - foreach (var entry in _loggingEvents.GetConsumingEnumerable(_loggingCancelationToken)) - { - HttpContext = entry.HttpContext; - ForwardLoggingEvent(entry.LoggingEvent, ThisType); - } - } - catch (OperationCanceledException ex) - { - //The thread was canceled before all entries could be forwarded and the collection completed. - ForwardInternalError("Subscriber task was canceled before completion.", ex, ThisType); - //Cancellation is called in the CompleteSubscriberTask so don't call that again. - } - catch (ThreadAbortException ex) - { - //Thread abort may occur on domain unload. - ForwardInternalError("Subscriber task was aborted.", ex, ThisType); - //Cannot recover from a thread abort so complete the task. - CompleteSubscriberTask(); - //The exception is swallowed because we don't want the client application - //to halt due to a logging issue. - } - catch (Exception ex) - { - //On exception, try to log the exception - ForwardInternalError("Subscriber task error in forwarding loop.", ex, ThisType); - //Any error in the loop is going to be some sort of extenuating circumstance from which we - //probably cannot recover anyway. Complete subscribing. - CompleteSubscriberTask(); - } - } - - #endregion Forwarding - - #region IDisposable Implementation - - private bool _disposed = false; - - //Implement IDisposable. - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (!_disposed) - { - if (disposing) - { - if (_loggingTask != null) - { - if (!(_loggingTask.IsCanceled || _loggingTask.IsCompleted || _loggingTask.IsFaulted)) - { - try - { - CompleteSubscriberTask(); - } - catch (Exception ex) - { - LogLog.Error(ThisType, "Exception Completing Subscriber Task in Dispose Method", ex); - } - } - try - { - _loggingTask.Dispose(); - } - catch (Exception ex) - { - LogLog.Error(ThisType, "Exception Disposing Logging Task", ex); - } - finally - { - _loggingTask = null; - } - } - if (_loggingEvents != null) - { - try - { - _loggingEvents.Dispose(); - } - catch (Exception ex) - { - LogLog.Error(ThisType, "Exception Disposing BlockingCollection", ex); - } - finally - { - _loggingEvents = null; - } - } - if (_loggingCancelationTokenSource != null) - { - try - { - _loggingCancelationTokenSource.Dispose(); - } - catch (Exception ex) - { - LogLog.Error(ThisType, "Exception Disposing CancellationTokenSource", ex); - } - finally - { - _loggingCancelationTokenSource = null; - } - } - } - _disposed = true; - } - } - - // Use C# destructor syntax for finalization code. - ~ParallelForwardingAppender() - { - // Simply call Dispose(false). - Dispose(false); - } - - #endregion IDisposable Implementation - } -} \ No newline at end of file +//namespace Umbraco.Core.Logging +//{ +// /// +// /// An asynchronous appender based on +// /// +// /// +// /// Borrowed from https://github.com/cjbhaines/Log4Net.Async - will reference Nuget packages directly in v8 +// /// +// [Obsolete("Use the Log4Net.Async.ParallelForwardingAppender instead this will be removed in future versions")] +// public class ParallelForwardingAppender : Log4Net.Async.ParallelForwardingAppender +// { +// } +//} \ No newline at end of file diff --git a/src/Umbraco.Core/Mandate.cs b/src/Umbraco.Core/Mandate.cs index 7ae47fd5c0..15a0e94bc7 100644 --- a/src/Umbraco.Core/Mandate.cs +++ b/src/Umbraco.Core/Mandate.cs @@ -8,6 +8,7 @@ namespace Umbraco.Core /// /// Helper class for mandating values, for example on method parameters. /// + [Obsolete] public static class Mandate { /// diff --git a/src/Umbraco.Core/Media/IImageUrlProvider.cs b/src/Umbraco.Core/Media/IImageUrlProvider.cs index 3854e1f1ec..29c0ae34ed 100644 --- a/src/Umbraco.Core/Media/IImageUrlProvider.cs +++ b/src/Umbraco.Core/Media/IImageUrlProvider.cs @@ -1,8 +1,14 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Umbraco.Core.Media { - public interface IImageUrlProvider + // note: because this interface is obsolete is is *not* IDiscoverable, and in case the + // PluginManager is asked to find types implementing this interface it will fall back + // to a complete scan. + + [Obsolete("IImageUrlProvider is no longer used and will be removed in future versions")] + public interface IImageUrlProvider // IDiscoverable { string Name { get; } string GetImageUrlFromMedia(int mediaId, IDictionary parameters); diff --git a/src/Umbraco.Core/Media/IThumbnailProvider.cs b/src/Umbraco.Core/Media/IThumbnailProvider.cs index a5be69b72e..18b8453324 100644 --- a/src/Umbraco.Core/Media/IThumbnailProvider.cs +++ b/src/Umbraco.Core/Media/IThumbnailProvider.cs @@ -1,6 +1,13 @@ -namespace Umbraco.Core.Media +using System; + +namespace Umbraco.Core.Media { - public interface IThumbnailProvider + // note: because this interface is obsolete is is *not* IDiscoverable, and in case the + // PluginManager is asked to find types implementing this interface it will fall back + // to a complete scan. + + [Obsolete("Thumbnails are generated by ImageProcessor, use that instead")] + public interface IThumbnailProvider // : IDiscoverable { bool CanProvideThumbnail(string fileUrl); string GetThumbnailUrl(string fileUrl); diff --git a/src/Umbraco.Core/Media/UploadAutoFillProperties.cs b/src/Umbraco.Core/Media/UploadAutoFillProperties.cs index 3a03932a72..e38c2adf3c 100644 --- a/src/Umbraco.Core/Media/UploadAutoFillProperties.cs +++ b/src/Umbraco.Core/Media/UploadAutoFillProperties.cs @@ -96,8 +96,7 @@ namespace Umbraco.Core.Media /// The property type alias. /// The filesystem-relative filepath, or null to clear properties. /// The stream containing the file data. - /// The file data as an image object. - public void Populate(IContentBase content, string propertyTypeAlias, string filepath, Stream filestream, Image image = null) + public void Populate(IContentBase content, string propertyTypeAlias, string filepath, Stream filestream) { if (content == null) throw new ArgumentNullException(nameof(content)); if (propertyTypeAlias == null) throw new ArgumentNullException(nameof(propertyTypeAlias)); @@ -110,7 +109,7 @@ namespace Umbraco.Core.Media if (autoFillConfig == null) return; // nothing // populate - Populate(content, autoFillConfig, filepath, filestream, image); + Populate(content, autoFillConfig, filepath, filestream); } /// @@ -157,8 +156,7 @@ namespace Umbraco.Core.Media /// /// The filesystem-relative filepath, or null to clear properties. /// The stream containing the file data. - /// The file data as an image object. - public void Populate(IContentBase content, IImagingAutoFillUploadField autoFillConfig, string filepath, Stream filestream, Image image = null) + public void Populate(IContentBase content, IImagingAutoFillUploadField autoFillConfig, string filepath, Stream filestream) { if (content == null) throw new ArgumentNullException(nameof(content)); if (autoFillConfig == null) throw new ArgumentNullException(nameof(autoFillConfig)); @@ -171,11 +169,7 @@ namespace Umbraco.Core.Media else { var extension = (Path.GetExtension(filepath) ?? "").TrimStart('.'); - Size? size; - if (image == null) - size = _mediaFileSystem.IsImageFile(extension) ? (Size?) _mediaFileSystem.GetDimensions(filestream) : null; - else - size = new Size(image.Width, image.Height); + var size = _mediaFileSystem.IsImageFile(extension) ? (Size?)_mediaFileSystem.GetDimensions(filestream) : null; SetProperties(content, autoFillConfig, size, filestream.Length, extension); } } diff --git a/src/Umbraco.Core/Models/ApplicationTree.cs b/src/Umbraco.Core/Models/ApplicationTree.cs index cc754ec694..2244d206d3 100644 --- a/src/Umbraco.Core/Models/ApplicationTree.cs +++ b/src/Umbraco.Core/Models/ApplicationTree.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Diagnostics; namespace Umbraco.Core.Models @@ -6,12 +7,13 @@ namespace Umbraco.Core.Models [DebuggerDisplay("Tree - {Title} ({ApplicationAlias})")] public class ApplicationTree { + private static readonly ConcurrentDictionary ResolvedTypes = new ConcurrentDictionary(); + /// /// Initializes a new instance of the class. /// public ApplicationTree() { } - /// /// Initializes a new instance of the class. /// @@ -25,14 +27,14 @@ namespace Umbraco.Core.Models /// The tree type. public ApplicationTree(bool initialize, int sortOrder, string applicationAlias, string alias, string title, string iconClosed, string iconOpened, string type) { - this.Initialize = initialize; - this.SortOrder = sortOrder; - this.ApplicationAlias = applicationAlias; - this.Alias = alias; - this.Title = title; - this.IconClosed = iconClosed; - this.IconOpened = iconOpened; - this.Type = type; + Initialize = initialize; + SortOrder = sortOrder; + ApplicationAlias = applicationAlias; + Alias = alias; + Title = title; + IconClosed = iconClosed; + IconOpened = iconOpened; + Type = type; } /// @@ -51,13 +53,13 @@ namespace Umbraco.Core.Models /// Gets the application alias. /// /// The application alias. - public string ApplicationAlias { get; private set; } + public string ApplicationAlias { get; } /// /// Gets the tree alias. /// /// The alias. - public string Alias { get; private set; } + public string Alias { get; } /// /// Gets or sets the tree title. @@ -93,7 +95,47 @@ namespace Umbraco.Core.Models { return _runtimeType ?? (_runtimeType = System.Type.GetType(Type)); } - + /// + /// Used to try to get and cache the tree type + /// + /// + /// + internal static Type TryGetType(string type) + { + try + { + return ResolvedTypes.GetOrAdd(type, s => + { + var result = System.Type.GetType(type); + if (result != null) + { + return result; + } + + //we need to implement a bit of a hack here due to some trees being renamed and backwards compat + var parts = type.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length != 2) + throw new InvalidOperationException("Could not resolve type"); + if (parts[1].Trim() != "umbraco" || parts[0].StartsWith("Umbraco.Web.Trees") == false || parts[0].EndsWith("Controller")) + throw new InvalidOperationException("Could not resolve type"); + + //if it's one of our controllers but it's not suffixed with "Controller" then add it and try again + var tempType = parts[0] + "Controller, umbraco"; + + result = System.Type.GetType(tempType); + if (result != null) + return result; + + throw new InvalidOperationException("Could not resolve type"); + }); + } + catch (InvalidOperationException) + { + //swallow, this is our own exception, couldn't find the type + // fixme bad use of exceptions here! + return null; + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index b2d816e0de..72668d801b 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -1,8 +1,5 @@ using System; -using System.Collections; -using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using System.Reflection; using System.Runtime.Serialization; @@ -287,8 +284,11 @@ namespace Umbraco.Core.Models /// /// Gets a value indicating whether the content has a published version. /// - public bool HasPublishedVersion { get { return PublishedVersionGuid != default(Guid); } } - + public bool HasPublishedVersion => PublishedVersionGuid != default(Guid); + + [IgnoreDataMember] + internal DateTime PublishedDate { get; set; } + public override void ResetDirtyProperties(bool rememberPreviouslyChangedProperties) { base.ResetDirtyProperties(rememberPreviouslyChangedProperties); diff --git a/src/Umbraco.Core/Models/ContentExtensions.cs b/src/Umbraco.Core/Models/ContentExtensions.cs index 4df84427cf..e4212a5202 100644 --- a/src/Umbraco.Core/Models/ContentExtensions.cs +++ b/src/Umbraco.Core/Models/ContentExtensions.cs @@ -457,14 +457,13 @@ namespace Umbraco.Core.Models return content.Properties .Where(property => propertyGroup.PropertyTypes .Select(propertyType => propertyType.Id) - .Contains(property.PropertyTypeId)) - .OrderBy(x => x.PropertyType.SortOrder); + .Contains(property.PropertyTypeId)); } /// /// Set property values by alias with an annonymous object /// - public static void PropertyValues(this IContent content, object value) + public static void PropertyValues(this IContentBase content, object value) { if (value == null) throw new Exception("No properties has been passed in"); @@ -520,7 +519,7 @@ namespace Umbraco.Core.Models /// The uploaded . public static void SetValue(this IContentBase content, string propertyTypeAlias, HttpPostedFileBase value) { - // ensure we get the filename without the path in IE in intranet mode + // ensure we get the filename without the path in IE in intranet mode // http://stackoverflow.com/questions/382464/httppostedfile-filename-different-from-ie var filename = value.FileName; var pos = filename.LastIndexOf(@"\", StringComparison.InvariantCulture); @@ -532,7 +531,7 @@ namespace Umbraco.Core.Models if (pos > 0) filename = filename.Substring(pos + 1); - // get a safe filename - should this be done by MediaHelper? + // get a safe & clean filename filename = IOHelper.SafeFileName(filename); if (string.IsNullOrWhiteSpace(filename)) return; filename = filename.ToLower(); // fixme - er... why? @@ -578,7 +577,7 @@ namespace Umbraco.Core.Models { if (filename == null || filestream == null) return; - // get a safe filename - should this be done by MediaHelper? + // get a safe & clean filename filename = IOHelper.SafeFileName(filename); if (string.IsNullOrWhiteSpace(filename)) return; filename = filename.ToLower(); // fixme - er... why? diff --git a/src/Umbraco.Core/Models/ContentTypeExtensions.cs b/src/Umbraco.Core/Models/ContentTypeExtensions.cs deleted file mode 100644 index 87cd881794..0000000000 --- a/src/Umbraco.Core/Models/ContentTypeExtensions.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core.Services; - -namespace Umbraco.Core.Models -{ - internal static class ContentTypeExtensions - { - /// - /// Gets all descendant content types of a specified content type. - /// - /// The content type. - /// The content type service. - /// The descendant content types. - /// Descendants corresponds to the parent-child relationship, and has - /// nothing to do with compositions, though a child should always be composed - /// of its parent. - public static IEnumerable Descendants(this TItem contentType, IContentTypeServiceBase contentTypeService) - where TItem : IContentTypeComposition - { - return contentTypeService.GetDescendants(contentType.Id, false); - } - - /// - /// Gets all descendant and self content types of a specified content type. - /// - /// The content type. - /// The content type service. - /// The descendant and self content types. - /// Descendants corresponds to the parent-child relationship, and has - /// nothing to do with compositions, though a child should always be composed - /// of its parent. - public static IEnumerable DescendantsAndSelf(this TItem contentType, IContentTypeServiceBase contentTypeService) - where TItem : IContentTypeComposition - { - return contentTypeService.GetDescendants(contentType.Id, true); - } - - /// - /// Gets all content types directly or indirectly composed of a specified content type. - /// - /// The content type. - /// The content type service. - /// The content types directly or indirectly composed of the content type. - /// This corresponds to the composition relationship and has nothing to do - /// with the parent-child relationship, though a child should always be composed of - /// its parent. - public static IEnumerable ComposedOf(this TItem contentType, IContentTypeServiceBase contentTypeService) - where TItem : IContentTypeComposition - { - return contentTypeService.GetComposedOf(contentType.Id); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Editors/ContentPropertyData.cs b/src/Umbraco.Core/Models/Editors/ContentPropertyData.cs index 14c8640fc9..e9fff32f25 100644 --- a/src/Umbraco.Core/Models/Editors/ContentPropertyData.cs +++ b/src/Umbraco.Core/Models/Editors/ContentPropertyData.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Umbraco.Core.Models.Editors { @@ -16,6 +12,10 @@ namespace Umbraco.Core.Models.Editors /// public class ContentPropertyData { + public ContentPropertyData(object value, PreValueCollection preValues) + : this(value, preValues, new Dictionary()) + { } + public ContentPropertyData(object value, PreValueCollection preValues, IDictionary additionalData) { Value = value; @@ -26,14 +26,17 @@ namespace Umbraco.Core.Models.Editors /// /// The value submitted for the property /// - public object Value { get; private set; } + public object Value { get; } - public PreValueCollection PreValues { get; private set; } + /// + /// The pre-value collection for the content property + /// + public PreValueCollection PreValues { get; } /// /// A dictionary containing any additional objects that are related to this property when saving /// - public ReadOnlyDictionary AdditionalData { get; private set; } - + public ReadOnlyDictionary AdditionalData { get; } + } } diff --git a/src/Umbraco.Core/Models/File.cs b/src/Umbraco.Core/Models/File.cs index e271774b8d..41838a3841 100644 --- a/src/Umbraco.Core/Models/File.cs +++ b/src/Umbraco.Core/Models/File.cs @@ -49,7 +49,10 @@ namespace Umbraco.Core.Models return path .Replace('\\', System.IO.Path.DirectorySeparatorChar) .Replace('/', System.IO.Path.DirectorySeparatorChar); - //.TrimStart(System.IO.Path.DirectorySeparatorChar); + + //Don't strip the start - this was a bug fixed in 7.3, see ScriptRepositoryTests.PathTests + //.TrimStart(System.IO.Path.DirectorySeparatorChar) + //.TrimStart('/'); } /// diff --git a/src/Umbraco.Core/Models/GridValue.cs b/src/Umbraco.Core/Models/GridValue.cs new file mode 100644 index 0000000000..87f6146af6 --- /dev/null +++ b/src/Umbraco.Core/Models/GridValue.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Umbraco.Core.Models +{ + /// + /// 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; } + + public class GridSection + { + [JsonProperty("grid")] + public string Grid { get; set; } + + [JsonProperty("rows")] + public IEnumerable Rows { get; set; } + } + + public class GridRow + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("id")] + public Guid Id { get; set; } + + [JsonProperty("areas")] + public IEnumerable Areas { get; set; } + + [JsonProperty("styles")] + public JToken Styles { get; set; } + + [JsonProperty("config")] + public JToken Config { get; set; } + } + + public class GridArea + { + [JsonProperty("grid")] + public string Grid { get; set; } + + [JsonProperty("controls")] + public IEnumerable Controls { get; set; } + + [JsonProperty("styles")] + public JToken Styles { get; set; } + + [JsonProperty("config")] + public JToken Config { get; set; } + } + + public class GridControl + { + [JsonProperty("value")] + public JToken Value { get; set; } + + [JsonProperty("editor")] + public GridEditor Editor { get; set; } + + [JsonProperty("styles")] + public JToken Styles { get; set; } + + [JsonProperty("config")] + public JToken Config { get; set; } + } + + public class GridEditor + { + [JsonProperty("alias")] + public string Alias { get; set; } + + [JsonProperty("view")] + public string View { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/IMacroProperty.cs b/src/Umbraco.Core/Models/IMacroProperty.cs index a6c1d9ca5f..8726f51317 100644 --- a/src/Umbraco.Core/Models/IMacroProperty.cs +++ b/src/Umbraco.Core/Models/IMacroProperty.cs @@ -1,3 +1,4 @@ +using System; using System.Runtime.Serialization; using Umbraco.Core.Models.EntityBase; @@ -11,6 +12,9 @@ namespace Umbraco.Core.Models [DataMember] int Id { get; set; } + [DataMember] + Guid Key { get; set; } + /// /// Gets or sets the Alias of the Property /// diff --git a/src/Umbraco.Core/Models/IMediaType.cs b/src/Umbraco.Core/Models/IMediaType.cs index 29e4b665ba..e8f7c16190 100644 --- a/src/Umbraco.Core/Models/IMediaType.cs +++ b/src/Umbraco.Core/Models/IMediaType.cs @@ -4,7 +4,7 @@ namespace Umbraco.Core.Models { /// /// Defines a ContentType, which Media is based on - /// public interface IMediaType : IContentTypeComposition { diff --git a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs index 898ce49ce2..ead07ce86d 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs @@ -20,6 +20,7 @@ namespace Umbraco.Core.Models.Identity public override void ConfigureMappings(IMapperConfiguration config) { config.CreateMap() + .ForMember(user => user.LastLoginDateUtc, expression => expression.MapFrom(user => user.LastLoginDate.ToUniversalTime())) .ForMember(user => user.Email, expression => expression.MapFrom(user => user.Email)) .ForMember(user => user.Id, expression => expression.MapFrom(user => user.Id)) .ForMember(user => user.LockoutEndDateUtc, expression => expression.MapFrom(user => user.IsLockedOut ? DateTime.MaxValue.ToUniversalTime() : (DateTime?) null)) diff --git a/src/Umbraco.Core/Models/Identity/IdentityUser.cs b/src/Umbraco.Core/Models/Identity/IdentityUser.cs index cba4fc514a..2e40d09cc1 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityUser.cs @@ -25,101 +25,89 @@ namespace Umbraco.Core.Models.Identity /// public IdentityUser() { - this.Claims = new List(); - this.Roles = new List(); - this.Logins = new List(); + Claims = new List(); + Roles = new List(); + Logins = new List(); } + /// + /// Last login date + /// + public virtual DateTime? LastLoginDateUtc { get; set; } + /// /// Email - /// /// public virtual string Email { get; set; } /// /// True if the email is confirmed, default is false - /// /// public virtual bool EmailConfirmed { get; set; } /// /// The salted/hashed form of the user password - /// /// public virtual string PasswordHash { get; set; } /// /// A random value that should change whenever a users credentials have changed (password changed, login removed) - /// /// public virtual string SecurityStamp { get; set; } /// /// PhoneNumber for the user - /// /// public virtual string PhoneNumber { get; set; } /// /// True if the phone number is confirmed, default is false - /// /// public virtual bool PhoneNumberConfirmed { get; set; } /// /// Is two factor enabled for the user - /// /// public virtual bool TwoFactorEnabled { get; set; } /// /// DateTime in UTC when lockout ends, any time in the past is considered not locked out. - /// /// public virtual DateTime? LockoutEndDateUtc { get; set; } /// /// Is lockout enabled for this user - /// /// public virtual bool LockoutEnabled { get; set; } /// /// Used to record failures for the purposes of lockout - /// /// public virtual int AccessFailedCount { get; set; } /// /// Navigation property for user roles - /// /// - public virtual ICollection Roles { get; private set; } + public virtual ICollection Roles { get; } /// /// Navigation property for user claims - /// /// - public virtual ICollection Claims { get; private set; } + public virtual ICollection Claims { get; } /// /// Navigation property for user logins - /// /// - public virtual ICollection Logins { get; private set; } + public virtual ICollection Logins { get; } /// /// User ID (Primary Key) - /// /// public virtual TKey Id { get; set; } /// /// User name - /// /// public virtual string UserName { get; set; } - - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Macro.cs b/src/Umbraco.Core/Models/Macro.cs index ef5479c316..58ee94bdc9 100644 --- a/src/Umbraco.Core/Models/Macro.cs +++ b/src/Umbraco.Core/Models/Macro.cs @@ -1,13 +1,10 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; using System.Reflection; using System.Runtime.Serialization; -using System.Text.RegularExpressions; -using Umbraco.Core.IO; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Strings; @@ -27,11 +24,12 @@ namespace Umbraco.Core.Models _addedProperties = new List(); _removedProperties = new List(); } - + /// /// Creates an item with pre-filled properties /// /// + /// /// /// /// @@ -43,10 +41,11 @@ namespace Umbraco.Core.Models /// /// /// - public Macro(int id, bool useInEditor, int cacheDuration, string @alias, string name, string controlType, string controlAssembly, string xsltPath, bool cacheByPage, bool cacheByMember, bool dontRender, string scriptPath) + public Macro(int id, Guid key, bool useInEditor, int cacheDuration, string @alias, string name, string controlType, string controlAssembly, string xsltPath, bool cacheByPage, bool cacheByMember, bool dontRender, string scriptPath) : this() { Id = id; + Key = key; UseInEditor = useInEditor; CacheDuration = cacheDuration; Alias = alias.ToCleanString(CleanStringType.Alias); diff --git a/src/Umbraco.Core/Models/MacroProperty.cs b/src/Umbraco.Core/Models/MacroProperty.cs index 0f29c18881..d3d82d10ad 100644 --- a/src/Umbraco.Core/Models/MacroProperty.cs +++ b/src/Umbraco.Core/Models/MacroProperty.cs @@ -15,7 +15,7 @@ namespace Umbraco.Core.Models { public MacroProperty() { - + _key = Guid.NewGuid(); } /// @@ -30,6 +30,7 @@ namespace Umbraco.Core.Models _alias = alias; _name = name; _sortOrder = sortOrder; + _key = Guid.NewGuid(); //try to get the new mapped parameter editor var mapped = LegacyParameterEditorAliasConverter.GetNewAliasFromLegacyAlias(editorAlias, false); @@ -45,16 +46,18 @@ namespace Umbraco.Core.Models /// Ctor for creating an existing property /// /// + /// /// /// /// /// - internal MacroProperty(int id, string @alias, string name, int sortOrder, string editorAlias) + internal MacroProperty(int id, Guid key, string @alias, string name, int sortOrder, string editorAlias) { _id = id; _alias = alias; _name = name; _sortOrder = sortOrder; + _key = key; //try to get the new mapped parameter editor var mapped = LegacyParameterEditorAliasConverter.GetNewAliasFromLegacyAlias(editorAlias, false); @@ -66,6 +69,7 @@ namespace Umbraco.Core.Models _editorAlias = editorAlias; } + private Guid _key; private string _alias; private string _name; private int _sortOrder; @@ -76,6 +80,7 @@ namespace Umbraco.Core.Models private class PropertySelectors { + public readonly PropertyInfo KeySelector = ExpressionHelper.GetPropertyInfo(x => x.Key); public readonly PropertyInfo AliasSelector = ExpressionHelper.GetPropertyInfo(x => x.Alias); public readonly PropertyInfo NameSelector = ExpressionHelper.GetPropertyInfo(x => x.Name); public readonly PropertyInfo SortOrderSelector = ExpressionHelper.GetPropertyInfo(x => x.SortOrder); @@ -83,6 +88,16 @@ namespace Umbraco.Core.Models public readonly PropertyInfo PropertyTypeSelector = ExpressionHelper.GetPropertyInfo(x => x.EditorAlias); } + /// + /// Gets or sets the Key of the Property + /// + [DataMember] + public Guid Key + { + get { return _key; } + set { SetPropertyValueAndDetectChanges(value, ref _key, Ps.Value.KeySelector); } + } + /// /// Gets or sets the Alias of the Property /// diff --git a/src/Umbraco.Core/Models/PropertyTypeCollection.cs b/src/Umbraco.Core/Models/PropertyTypeCollection.cs index 38abd0c57d..a06e6d737d 100644 --- a/src/Umbraco.Core/Models/PropertyTypeCollection.cs +++ b/src/Umbraco.Core/Models/PropertyTypeCollection.cs @@ -69,6 +69,7 @@ namespace Umbraco.Core.Models OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } + //TODO: Instead of 'new' this should explicitly implement one of the collection interfaces members internal new void Add(PropertyType item) { using (new WriteLock(_addLocker)) diff --git a/src/Umbraco.Core/Models/PublicAccessEntry.cs b/src/Umbraco.Core/Models/PublicAccessEntry.cs index 1ed15a8fb4..5989f885cb 100644 --- a/src/Umbraco.Core/Models/PublicAccessEntry.cs +++ b/src/Umbraco.Core/Models/PublicAccessEntry.cs @@ -110,6 +110,12 @@ namespace Umbraco.Core.Models _ruleCollection.Clear(); } + + internal void ClearRemovedRules() + { + _removedRules.Clear(); + } + [DataMember] public int LoginNodeId { diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentEnumerable.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentEnumerable.cs new file mode 100644 index 0000000000..dc17265bb9 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentEnumerable.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Models.PublishedContent +{ + // fixme imported from 7.6 needs better explaination of what it is + + /// + /// The published content enumerable, this model is to allow ToString to be overriden for value converters to support legacy requests for string values + /// + public class PublishedContentEnumerable : IEnumerable + { + /// + /// The items in the collection + /// + private readonly IEnumerable _items; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The published content items + /// + public PublishedContentEnumerable(IEnumerable publishedContent) + { + _items = publishedContent ?? throw new ArgumentNullException(nameof(publishedContent)); + } + + /// + /// The ToString method to convert the objects back to CSV + /// + /// + /// The . + /// + public override string ToString() + { + return string.Join(",", _items.Select(x => x.Id)); + } + + /// + /// The get enumerator. + /// + /// + /// The . + /// + public IEnumerator GetEnumerator() + { + return _items.GetEnumerator(); + } + + /// + /// The get enumerator. + /// + /// + /// The . + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtended.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtended.cs index f024e084f1..f5b9d07ebb 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtended.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtended.cs @@ -1,11 +1,11 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.ObjectModel; +using System.ComponentModel; using System.Linq; namespace Umbraco.Core.Models.PublishedContent { - + [TypeConverter(typeof(PublishedContentTypeConverter))] public class PublishedContentExtended : PublishedContentWrapped, IPublishedContentExtended { #region Constructor @@ -84,6 +84,10 @@ namespace Umbraco.Core.Models.PublishedContent } #endregion - } + public override string ToString() + { + return Id.ToString(); + } + } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeConverter.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeConverter.cs new file mode 100644 index 0000000000..e0d019b738 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeConverter.cs @@ -0,0 +1,37 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using Umbraco.Core.Plugins; + +namespace Umbraco.Core.Models.PublishedContent +{ + internal class PublishedContentTypeConverter : TypeConverter + { + private static readonly Type[] ConvertableTypes = { typeof(int) }; + + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + { + return ConvertableTypes.Any(x => TypeHelper.IsTypeAssignableFrom(x, destinationType)) + || base.CanConvertFrom(context, destinationType); + } + + public override object ConvertTo( + ITypeDescriptorContext context, + CultureInfo culture, + object value, + Type destinationType) + { + var publishedContent = value as IPublishedContent; + if (publishedContent == null) + return null; + + if (TypeHelper.IsTypeAssignableFrom(destinationType)) + { + return publishedContent.Id; + } + + return base.ConvertTo(context, culture, value, destinationType); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs index c1766ffd03..22a41bcf50 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs @@ -1,11 +1,9 @@ using System; -using System.Globalization; using System.Linq; using System.Xml.Linq; using System.Xml.XPath; using Umbraco.Core.DI; using Umbraco.Core.PropertyEditors; -using Umbraco.Core.Xml; namespace Umbraco.Core.Models.PublishedContent { @@ -36,6 +34,8 @@ namespace Umbraco.Core.Models.PublishedContent PropertyEditorAlias = propertyType.PropertyEditorAlias; } + + // fixme remove /* /// /// Initializes a new instance of the class with an existing @@ -136,7 +136,7 @@ namespace Umbraco.Core.Models.PublishedContent /// /// Gets or sets a value indicating whether the property is an Umbraco-defined property. /// - internal bool IsUmbraco { get; private set; } + internal bool IsUmbraco { get; } #endregion diff --git a/src/Umbraco.Core/Models/Range.cs b/src/Umbraco.Core/Models/Range.cs new file mode 100644 index 0000000000..3095d6342d --- /dev/null +++ b/src/Umbraco.Core/Models/Range.cs @@ -0,0 +1,52 @@ +using System; +namespace Umbraco.Core.Models +{ + // The Range class. Adapted from http://stackoverflow.com/questions/5343006/is-there-a-c-sharp-type-for-representing-an-integer-range + /// Generic parameter. + public class Range where T : IComparable + { + /// Minimum value of the range. + public T Minimum { get; set; } + + /// Maximum value of the range. + public T Maximum { get; set; } + + /// Presents the Range in readable format. + /// String representation of the Range + public override string ToString() + { + return string.Format("{0},{1}", this.Minimum, this.Maximum); + } + + /// Determines if the range is valid. + /// True if range is valid, else false + public bool IsValid() + { + return this.Minimum.CompareTo(this.Maximum) <= 0; + } + + /// Determines if the provided value is inside the range. + /// The value to test + /// True if the value is inside Range, else false + public bool ContainsValue(T value) + { + return (this.Minimum.CompareTo(value) <= 0) && (value.CompareTo(this.Maximum) <= 0); + } + + /// Determines if this Range is inside the bounds of another range. + /// The parent range to test on + /// True if range is inclusive, else false + public bool IsInsideRange(Range range) + { + return this.IsValid() && range.IsValid() && range.ContainsValue(this.Minimum) && range.ContainsValue(this.Maximum); + } + + /// Determines if another range is inside the bounds of this range. + /// The child range to test + /// True if range is inside, else false + public bool ContainsRange(Range range) + { + return this.IsValid() && range.IsValid() && this.ContainsValue(range.Minimum) && this.ContainsValue(range.Maximum); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/DataTypeDto.cs b/src/Umbraco.Core/Models/Rdbms/DataTypeDto.cs index 148116d70e..16575d01c9 100644 --- a/src/Umbraco.Core/Models/Rdbms/DataTypeDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/DataTypeDto.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core.Models.Rdbms internal class DataTypeDto { [Column("pk")] - [PrimaryKeyColumn(IdentitySeed = 30)] + [PrimaryKeyColumn(IdentitySeed = 40)] public int PrimaryKey { get; set; } [Column("nodeId")] diff --git a/src/Umbraco.Core/Models/Rdbms/DataTypePreValueDto.cs b/src/Umbraco.Core/Models/Rdbms/DataTypePreValueDto.cs index 69c9e17dbd..2d66ca918d 100644 --- a/src/Umbraco.Core/Models/Rdbms/DataTypePreValueDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/DataTypePreValueDto.cs @@ -10,7 +10,7 @@ namespace Umbraco.Core.Models.Rdbms internal class DataTypePreValueDto { [Column("id")] - [PrimaryKeyColumn(IdentitySeed = 6)] + [PrimaryKeyColumn(IdentitySeed = 10)] public int Id { get; set; } [Column("datatypeNodeId")] diff --git a/src/Umbraco.Core/Models/Rdbms/DocumentPublishedReadOnlyDto.cs b/src/Umbraco.Core/Models/Rdbms/DocumentPublishedReadOnlyDto.cs index 788e321877..83c5c46f2b 100644 --- a/src/Umbraco.Core/Models/Rdbms/DocumentPublishedReadOnlyDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/DocumentPublishedReadOnlyDto.cs @@ -1,6 +1,5 @@ using System; using NPoco; -using Umbraco.Core.Persistence; namespace Umbraco.Core.Models.Rdbms { @@ -20,5 +19,8 @@ namespace Umbraco.Core.Models.Rdbms [Column("newest")] public bool Newest { get; set; } + + [Column("updateDate")] + public DateTime VersionDate { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/MacroDto.cs b/src/Umbraco.Core/Models/Rdbms/MacroDto.cs index 1affe3a04e..1c4f281bf5 100644 --- a/src/Umbraco.Core/Models/Rdbms/MacroDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/MacroDto.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using NPoco; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.DatabaseAnnotations; @@ -14,6 +15,10 @@ namespace Umbraco.Core.Models.Rdbms [PrimaryKeyColumn] public int Id { 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; } diff --git a/src/Umbraco.Core/Models/Rdbms/MacroPropertyDto.cs b/src/Umbraco.Core/Models/Rdbms/MacroPropertyDto.cs index 6d10315ec1..f351618fbe 100644 --- a/src/Umbraco.Core/Models/Rdbms/MacroPropertyDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/MacroPropertyDto.cs @@ -1,5 +1,5 @@ -using NPoco; -using Umbraco.Core.Persistence; +using System; +using NPoco; using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Models.Rdbms @@ -13,6 +13,11 @@ namespace Umbraco.Core.Models.Rdbms [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; } + [Column("editorAlias")] public string EditorAlias { get; set; } diff --git a/src/Umbraco.Core/Models/Rdbms/MemberDto.cs b/src/Umbraco.Core/Models/Rdbms/MemberDto.cs index 35d5db1042..de4cc50382 100644 --- a/src/Umbraco.Core/Models/Rdbms/MemberDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/MemberDto.cs @@ -23,6 +23,7 @@ namespace Umbraco.Core.Models.Rdbms [Column("LoginName")] [Length(1000)] [Constraint(Default = "''")] + [Index(IndexTypes.NonClustered, Name = "IX_cmsMember_LoginName")] public string LoginName { get; set; } [Column("Password")] diff --git a/src/Umbraco.Core/Models/Rdbms/NodeDto.cs b/src/Umbraco.Core/Models/Rdbms/NodeDto.cs index b0860ea34a..26a97d251e 100644 --- a/src/Umbraco.Core/Models/Rdbms/NodeDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/NodeDto.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core.Models.Rdbms [ExplicitColumns] internal class NodeDto { - public const int NodeIdSeed = 1050; + public const int NodeIdSeed = 1060; [Column("id")] [PrimaryKeyColumn(Name = "PK_structure", IdentitySeed = NodeIdSeed)] @@ -36,6 +36,7 @@ namespace Umbraco.Core.Models.Rdbms [Column("path")] [Length(150)] + [Index(IndexTypes.NonClustered, Name = "IX_umbracoNodePath")] public string Path { get; set; } [Column("sortOrder")] diff --git a/src/Umbraco.Core/Models/Rdbms/PropertyDataDto.cs b/src/Umbraco.Core/Models/Rdbms/PropertyDataDto.cs index c79daaf43d..12ff203351 100644 --- a/src/Umbraco.Core/Models/Rdbms/PropertyDataDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/PropertyDataDto.cs @@ -39,16 +39,8 @@ namespace Umbraco.Core.Models.Rdbms [NullSetting(NullSetting = NullSettings.Null)] public decimal? Decimal { - get - { - return _decimalValue; - } - set - { - // need to normalize the value (change the scaling factor and remove trailing zeroes) - // because the underlying database probably has messed with the scaling factor. - _decimalValue = value.HasValue ? (decimal?) value.Value.Normalize() : null; - } + get => _decimalValue; + set => _decimalValue = value?.Normalize(); } [Column("dataDate")] diff --git a/src/Umbraco.Core/Models/Rdbms/RelationDto.cs b/src/Umbraco.Core/Models/Rdbms/RelationDto.cs index 44f0199b17..0809dc0d1a 100644 --- a/src/Umbraco.Core/Models/Rdbms/RelationDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/RelationDto.cs @@ -17,6 +17,7 @@ namespace Umbraco.Core.Models.Rdbms [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")] diff --git a/src/Umbraco.Core/Models/Rdbms/RelationTypeDto.cs b/src/Umbraco.Core/Models/Rdbms/RelationTypeDto.cs index c927fcf455..d2a8086a5b 100644 --- a/src/Umbraco.Core/Models/Rdbms/RelationTypeDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/RelationTypeDto.cs @@ -16,6 +16,10 @@ namespace Umbraco.Core.Models.Rdbms [PrimaryKeyColumn(IdentitySeed = NodeIdSeed)] public int Id { get; set; } + [Column("typeUniqueId")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_UniqueId")] + public Guid UniqueId { get; set; } + [Column("dual")] public bool Dual { get; set; } @@ -26,11 +30,13 @@ namespace Umbraco.Core.Models.Rdbms public Guid ChildObjectType { get; set; } [Column("name")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_name")] public string Name { get; set; } [Column("alias")] [NullSetting(NullSetting = NullSettings.Null)] [Length(100)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_alias")] public string Alias { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/UmbracoDeployChecksumDto.cs b/src/Umbraco.Core/Models/Rdbms/UmbracoDeployChecksumDto.cs deleted file mode 100644 index a012d85563..0000000000 --- a/src/Umbraco.Core/Models/Rdbms/UmbracoDeployChecksumDto.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using NPoco; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.DatabaseAnnotations; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; - -namespace Umbraco.Core.Models.Rdbms -{ - [TableName("umbracoDeployChecksum")] - [PrimaryKey("id")] - [ExplicitColumns] - internal class UmbracoDeployChecksumDto - { - [Column("id")] - [PrimaryKeyColumn(Name = "PK_umbracoDeployChecksum")] - public int Id { get; set; } - - [Column("entityType")] - [Length(32)] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoDeployChecksum", ForColumns = "entityType,entityGuid,entityPath")] - public string EntityType { get; set; } - - [Column("entityGuid")] - [NullSetting(NullSetting = NullSettings.Null)] - public Guid EntityGuid { get; set; } - - [Column("entityPath")] - [Length(256)] - [NullSetting(NullSetting = NullSettings.Null)] - public string EntityPath { get; set; } - - [Column("localChecksum")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Length(32)] - public string LocalChecksum { get; set; } - - [Column("compositeChecksum")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(32)] - public string CompositeChecksum { get; set; } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/UmbracoDeployDependencyDto.cs b/src/Umbraco.Core/Models/Rdbms/UmbracoDeployDependencyDto.cs deleted file mode 100644 index 6ec19a513d..0000000000 --- a/src/Umbraco.Core/Models/Rdbms/UmbracoDeployDependencyDto.cs +++ /dev/null @@ -1,27 +0,0 @@ -using NPoco; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.DatabaseAnnotations; - -namespace Umbraco.Core.Models.Rdbms -{ - [TableName("umbracoDeployDependency")] - [ExplicitColumns] - internal class UmbracoDeployDependencyDto - { - [Column("sourceId")] - [PrimaryKeyColumn(AutoIncrement = false, Clustered = true, Name = "PK_umbracoDeployDependency", OnColumns = "sourceId, targetId")] - [ForeignKey(typeof(UmbracoDeployChecksumDto), Name = "FK_umbracoDeployDependency_umbracoDeployChecksum_id1")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public int SourceId { get; set; } - - [Column("targetId")] - [ForeignKey(typeof(UmbracoDeployChecksumDto), Name = "FK_umbracoDeployDependency_umbracoDeployChecksum_id2")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public int TargetId { get; set; } - - [Column("mode")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public int Mode { get; set; } - - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/User2NodePermissionDto.cs b/src/Umbraco.Core/Models/Rdbms/User2NodePermissionDto.cs index 117f88fe66..8234c0973c 100644 --- a/src/Umbraco.Core/Models/Rdbms/User2NodePermissionDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/User2NodePermissionDto.cs @@ -16,6 +16,7 @@ namespace Umbraco.Core.Models.Rdbms [Column("nodeId")] [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.NonClustered, Name = "IX_umbracoUser2NodePermission_nodeId")] public int NodeId { get; set; } [Column("permission")] diff --git a/src/Umbraco.Core/Models/UmbracoEntityExtensions.cs b/src/Umbraco.Core/Models/UmbracoEntityExtensions.cs index d0932e2b26..1a0b3b2271 100644 --- a/src/Umbraco.Core/Models/UmbracoEntityExtensions.cs +++ b/src/Umbraco.Core/Models/UmbracoEntityExtensions.cs @@ -1,15 +1,118 @@ using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; -using System.Web.UI.WebControls; +using System.IO; +using Umbraco.Core.Logging; using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Models.Rdbms; namespace Umbraco.Core.Models { internal static class UmbracoEntityExtensions { + /// + /// 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(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(new[] { ',' }, 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}"); + } + } + + /// + /// 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; + + if (entity.Path.IsNullOrWhiteSpace()) + return false; + + var pathParts = entity.Path.Split(new[] { ',' }, 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; + } + + 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.Warn(typeof(UmbracoEntityExtensions), $"The content item {entity.Id} has an invalid path: {entity.Path} with parentID: {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); + } + } + } + + /// + /// When resolved from EntityService this checks if the entity has the HasChildren flag + /// + /// + /// public static bool HasChildren(this IUmbracoEntity entity) { if (entity.AdditionalData.ContainsKey("HasChildren")) @@ -22,7 +125,6 @@ namespace Umbraco.Core.Models } return false; } - public static object GetAdditionalDataValueIgnoreCase(this IUmbracoEntity entity, string key, object defaultVal) { @@ -30,6 +132,11 @@ namespace Umbraco.Core.Models return entity.AdditionalData.GetValueIgnoreCase(key, defaultVal); } + /// + /// When resolved from EntityService this checks if the entity has the IsContainer flag + /// + /// + /// public static bool IsContainer(this IUmbracoEntity entity) { if (entity.AdditionalData.ContainsKeyIgnoreCase("IsContainer") == false) return false; @@ -40,6 +147,5 @@ namespace Umbraco.Core.Models } return false; } - } } diff --git a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs index 2be2010301..0df7a21e57 100644 --- a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs +++ b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs @@ -1,4 +1,6 @@ -using Umbraco.Core.CodeAnnotations; +using System; +using System.ComponentModel; +using Umbraco.Core.CodeAnnotations; namespace Umbraco.Core.Models { @@ -17,12 +19,14 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.ContentItemType)] [FriendlyName("Content Item Type")] + [Obsolete("This is not used and will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] ContentItemType, /// /// Root /// - [UmbracoObjectType(Constants.ObjectTypes.SystemRoot)] + [UmbracoObjectType(Constants.ObjectTypes.SystemRoot)] [FriendlyName("Root")] ROOT, @@ -31,6 +35,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.Document, typeof(IContent))] [FriendlyName("Document")] + [UmbracoUdiType(Constants.UdiEntityType.Document)] Document, /// @@ -38,6 +43,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.Media, typeof(IMedia))] [FriendlyName("Media")] + [UmbracoUdiType(Constants.UdiEntityType.Media)] Media, /// @@ -45,6 +51,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.MemberType, typeof(IMemberType))] [FriendlyName("Member Type")] + [UmbracoUdiType(Constants.UdiEntityType.MemberType)] MemberType, /// @@ -52,6 +59,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.Template, typeof(ITemplate))] [FriendlyName("Template")] + [UmbracoUdiType(Constants.UdiEntityType.Template)] Template, /// @@ -59,6 +67,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.MemberGroup)] [FriendlyName("Member Group")] + [UmbracoUdiType(Constants.UdiEntityType.MemberGroup)] MemberGroup, //TODO: What is a 'Content Item' supposed to be??? @@ -67,6 +76,8 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.ContentItem)] [FriendlyName("Content Item")] + [Obsolete("This is not used and will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] ContentItem, /// @@ -74,6 +85,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.MediaType, typeof(IMediaType))] [FriendlyName("Media Type")] + [UmbracoUdiType(Constants.UdiEntityType.MediaType)] MediaType, /// @@ -81,13 +93,14 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.DocumentType, typeof(IContentType))] [FriendlyName("Document Type")] + [UmbracoUdiType(Constants.UdiEntityType.DocumentType)] DocumentType, /// /// Recycle Bin /// [UmbracoObjectType(Constants.ObjectTypes.ContentRecycleBin)] - [FriendlyName("Recycle Bin")] + [FriendlyName("Recycle Bin")] RecycleBin, /// @@ -95,6 +108,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.Stylesheet)] [FriendlyName("Stylesheet")] + [UmbracoUdiType(Constants.UdiEntityType.Stylesheet)] Stylesheet, /// @@ -102,6 +116,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.Member, typeof(IMember))] [FriendlyName("Member")] + [UmbracoUdiType(Constants.UdiEntityType.Member)] Member, /// @@ -109,6 +124,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.DataType, typeof(IDataTypeDefinition))] [FriendlyName("Data Type")] + [UmbracoUdiType(Constants.UdiEntityType.DataType)] DataType, /// @@ -116,6 +132,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.DocumentTypeContainer)] [FriendlyName("Document Type Container")] + [UmbracoUdiType(Constants.UdiEntityType.DocumentTypeContainer)] DocumentTypeContainer, /// @@ -123,6 +140,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.MediaTypeContainer)] [FriendlyName("Media Type Container")] + [UmbracoUdiType(Constants.UdiEntityType.MediaTypeContainer)] MediaTypeContainer, /// @@ -130,8 +148,36 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.DataTypeContainer)] [FriendlyName("Data Type Container")] - DataTypeContainer + [UmbracoUdiType(Constants.UdiEntityType.DataTypeContainer)] + DataTypeContainer, + /// + /// Relation type + /// + [UmbracoObjectType(Constants.ObjectTypes.RelationType)] + [FriendlyName("Relation Type")] + [UmbracoUdiType(Constants.UdiEntityType.RelationType)] + RelationType, + /// + /// Forms Form + /// + [UmbracoObjectType(Constants.ObjectTypes.FormsForm)] + [FriendlyName("Form")] + FormsForm, + + /// + /// Forms PreValue + /// + [UmbracoObjectType(Constants.ObjectTypes.FormsPreValue)] + [FriendlyName("PreValue")] + FormsPreValue, + + /// + /// Forms DataSource + /// + [UmbracoObjectType(Constants.ObjectTypes.FormsDataSource)] + [FriendlyName("DataSource")] + FormsDataSource } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/UmbracoObjectTypesExtensions.cs b/src/Umbraco.Core/Models/UmbracoObjectTypesExtensions.cs index 34ee29b96f..5f92e6425e 100644 --- a/src/Umbraco.Core/Models/UmbracoObjectTypesExtensions.cs +++ b/src/Umbraco.Core/Models/UmbracoObjectTypesExtensions.cs @@ -12,6 +12,7 @@ namespace Umbraco.Core.Models { //MUST be concurrent to avoid thread collisions! private static readonly ConcurrentDictionary UmbracoObjectTypeCache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary UmbracoObjectTypeUdiCache = new ConcurrentDictionary(); /// /// Get an UmbracoObjectTypes value from it's name @@ -43,6 +44,21 @@ namespace Umbraco.Core.Models return umbracoObjectType; } + public static string GetUdiType(Guid guid) + { + var umbracoObjectType = Constants.UdiEntityType.Unknown; + + foreach (var name in Enum.GetNames(typeof(UmbracoObjectTypes))) + { + var objType = GetUmbracoObjectType(name); + if (objType.GetGuid() == guid) + { + umbracoObjectType = GetUdiType(objType); + } + } + return umbracoObjectType; + } + /// /// Extension method for the UmbracoObjectTypes enum to return the enum GUID /// @@ -68,6 +84,26 @@ namespace Umbraco.Core.Models }); } + public static string GetUdiType(this UmbracoObjectTypes umbracoObjectType) + { + return UmbracoObjectTypeUdiCache.GetOrAdd(umbracoObjectType, types => + { + var type = typeof(UmbracoObjectTypes); + var memInfo = type.GetMember(umbracoObjectType.ToString()); + var attributes = memInfo[0].GetCustomAttributes(typeof(UmbracoUdiTypeAttribute), + false); + + if (attributes.Length == 0) + return Constants.UdiEntityType.Unknown; + + var attribute = ((UmbracoUdiTypeAttribute)attributes[0]); + if (attribute == null) + return Constants.UdiEntityType.Unknown; + + return attribute.UdiType; + }); + } + /// /// Extension method for the UmbracoObjectTypes enum to return the enum name /// diff --git a/src/Umbraco.Core/NamedUdiRange.cs b/src/Umbraco.Core/NamedUdiRange.cs new file mode 100644 index 0000000000..c87e5db82e --- /dev/null +++ b/src/Umbraco.Core/NamedUdiRange.cs @@ -0,0 +1,34 @@ +namespace Umbraco.Core +{ + /// + /// Represents a complemented with a name. + /// + public class NamedUdiRange : UdiRange + { + /// + /// Initializes a new instance of the class with a and an optional selector. + /// + /// A . + /// An optional selector. + public NamedUdiRange(Udi udi, string selector = Constants.DeploySelector.This) + : base(udi, selector) + { } + + /// + /// Initializes a new instance of the class with a , a name, and an optional selector. + /// + /// A . + /// A name. + /// An optional selector. + public NamedUdiRange(Udi udi, string name, string selector = Constants.DeploySelector.This) + : base(udi, selector) + { + Name = name; + } + + /// + /// Gets or sets the name of the range. + /// + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/ObjectExtensions.cs b/src/Umbraco.Core/ObjectExtensions.cs index 4a06a94160..c0517706b8 100644 --- a/src/Umbraco.Core/ObjectExtensions.cs +++ b/src/Umbraco.Core/ObjectExtensions.cs @@ -469,19 +469,19 @@ namespace Umbraco.Core /// /// /// - internal static IDictionary ToDictionary(this T o, + public static IDictionary ToDictionary(this T o, params Expression>[] ignoreProperties) { return o.ToDictionary(ignoreProperties.Select(e => o.GetPropertyInfo(e)).Select(propInfo => propInfo.Name).ToArray()); } - /// - /// Turns object into dictionary - /// - /// - /// Properties to ignore - /// - internal static IDictionary ToDictionary(this object o, params string[] ignoreProperties) + /// + /// Turns object into dictionary + /// + /// + /// Properties to ignore + /// + public static IDictionary ToDictionary(this object o, params string[] ignoreProperties) { if (o != null) { @@ -648,5 +648,10 @@ namespace Umbraco.Core return "[GetPropertyValueException]"; } } + + internal static Guid AsGuid(this object value) + { + return value is Guid ? (Guid) value : Guid.Empty; + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/OrderedHashSet.cs b/src/Umbraco.Core/OrderedHashSet.cs new file mode 100644 index 0000000000..2fd545c915 --- /dev/null +++ b/src/Umbraco.Core/OrderedHashSet.cs @@ -0,0 +1,50 @@ +using System.Collections.ObjectModel; + +namespace Umbraco.Core +{ + /// + /// A custom collection similar to HashSet{T} which only contains unique items, however this collection keeps items in order + /// and is customizable to keep the newest or oldest equatable item + /// + /// + internal class OrderedHashSet : KeyedCollection + { + private readonly bool _keepOldest; + + public OrderedHashSet(bool keepOldest = true) + { + _keepOldest = keepOldest; + } + + protected override void InsertItem(int index, T item) + { + if (Dictionary == null) + { + base.InsertItem(index, item); + } + else + { + var exists = Dictionary.ContainsKey(item); + + //if we want to keep the newest, then we need to remove the old item and add the new one + if (exists == false) + { + base.InsertItem(index, item); + } + else if(_keepOldest == false) + { + if (Remove(item)) + { + index--; + } + base.InsertItem(index, item); + } + } + } + + protected override T GetKeyForItem(T item) + { + return item; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Packaging/Models/UninstallationSummary.cs b/src/Umbraco.Core/Packaging/Models/UninstallationSummary.cs new file mode 100644 index 0000000000..e40d453fdf --- /dev/null +++ b/src/Umbraco.Core/Packaging/Models/UninstallationSummary.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Packaging; + +namespace Umbraco.Core.Packaging.Models +{ + [Serializable] + [DataContract(IsReference = true)] + internal class UninstallationSummary + { + public MetaData MetaData { get; set; } + public IEnumerable DataTypesUninstalled { get; set; } + public IEnumerable LanguagesUninstalled { get; set; } + public IEnumerable DictionaryItemsUninstalled { get; set; } + public IEnumerable MacrosUninstalled { get; set; } + public IEnumerable FilesUninstalled { get; set; } + public IEnumerable TemplatesUninstalled { get; set; } + public IEnumerable ContentTypesUninstalled { get; set; } + public IEnumerable StylesheetsUninstalled { get; set; } + public IEnumerable ContentUninstalled { get; set; } + public bool PackageUninstalled { get; set; } + } + + internal static class UninstallationSummaryExtentions + { + public static UninstallationSummary InitEmpty(this UninstallationSummary summary) + { + summary.ContentUninstalled = new List(); + summary.ContentTypesUninstalled = new List(); + summary.DataTypesUninstalled = new List(); + summary.DictionaryItemsUninstalled = new List(); + summary.FilesUninstalled = new List(); + summary.LanguagesUninstalled = new List(); + summary.MacrosUninstalled = new List(); + summary.MetaData = new MetaData(); + summary.TemplatesUninstalled = new List(); + summary.PackageUninstalled = false; + return summary; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/DatabaseScope.cs b/src/Umbraco.Core/Persistence/DatabaseScope.cs deleted file mode 100644 index 72d2f53a8f..0000000000 --- a/src/Umbraco.Core/Persistence/DatabaseScope.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Threading; - -namespace Umbraco.Core.Persistence -{ - public class DatabaseScope : IDatabaseScope, IDisposeOnRequestEnd // implies IDisposable - { - private readonly DatabaseScope _parent; - private readonly IDatabaseScopeAccessor _accessor; - private readonly IUmbracoDatabaseFactory _factory; - private IUmbracoDatabase _database; - private bool _isParent; - private int _disposed; - private bool _disposeDatabase; - - // can specify a database to create a "substitute" scope eg for deploy - oh my - - internal DatabaseScope(IDatabaseScopeAccessor accessor, IUmbracoDatabaseFactory factory, IUmbracoDatabase database = null) - { - _accessor = accessor; - _factory = factory; - _database = database; - _parent = _accessor.Scope; - if (_parent != null) _parent._isParent = true; - _accessor.Scope = this; - } - - public IUmbracoDatabase Database - { - get - { - if (Interlocked.CompareExchange(ref _disposed, 0, 0) != 0) - throw new ObjectDisposedException(null, "Cannot access a disposed object."); - - if (_database != null) return _database; - if (_parent != null) return _parent.Database; - _database = _factory.CreateDatabase(); - _disposeDatabase = true; - return _database; - } - } - - public void Dispose() - { - if (_isParent) - throw new InvalidOperationException("Cannot dispose a parent scope."); - - if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0) - throw new ObjectDisposedException(null, "Cannot access a disposed object."); - - if (_disposeDatabase) - _database.Dispose(); - - _accessor.Scope = _parent; - if (_parent != null) _parent._isParent = false; - - GC.SuppressFinalize(this); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/DbConnectionExtensions.cs b/src/Umbraco.Core/Persistence/DbConnectionExtensions.cs index e3d73cf087..97b5d987ab 100644 --- a/src/Umbraco.Core/Persistence/DbConnectionExtensions.cs +++ b/src/Umbraco.Core/Persistence/DbConnectionExtensions.cs @@ -1,7 +1,10 @@ using System; using System.Data; using System.Data.Common; +using System.Data.SqlClient; +using System.Data.SqlServerCe; using System.Linq; +using MySql.Data.MySqlClient; using Umbraco.Core.DI; using Umbraco.Core.Logging; using Umbraco.Core.Persistence.FaultHandling; @@ -92,5 +95,37 @@ namespace Umbraco.Core.Persistence return unwrapped; } + + public static string GetConnStringExSecurityInfo(this IDbConnection connection) + { + try + { + if (connection is SqlConnection) + { + var builder = new SqlConnectionStringBuilder(connection.ConnectionString); + return $"DataSource: {builder.DataSource}, InitialCatalog: {builder.InitialCatalog}"; + } + + if (connection is SqlCeConnection) + { + var builder = new SqlCeConnectionStringBuilder(connection.ConnectionString); + return $"DataSource: {builder.DataSource}"; + } + + if (connection is MySqlConnection) + { + var builder = new MySqlConnectionStringBuilder(connection.ConnectionString); + return $"Server: {builder.Server}, Database: {builder.Database}"; + } + } + catch (Exception ex) + { + Current.Logger.Warn(typeof(DbConnectionExtensions), + "Could not resolve connection string parameters", ex); + return "(Could not resolve)"; + } + + throw new ArgumentException($"The connection type {connection.GetType()} is not supported"); + } } } diff --git a/src/Umbraco.Core/Persistence/Factories/ContentFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentFactory.cs index 43f77a8849..075f008393 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentFactory.cs @@ -27,15 +27,28 @@ namespace Umbraco.Core.Persistence.Factories #region Implementation of IEntityFactory - public IContent BuildEntity(DocumentDto dto) + /// + /// Builds a IContent item from the dto(s) and content type + /// + /// + /// This DTO can contain all of the information to build an IContent item, however in cases where multiple entities are being built, + /// a separate publishedDto entity will be supplied in place of the 's own + /// ResultColumn DocumentPublishedReadOnlyDto + /// + /// + /// + /// When querying for multiple content items the main DTO will not contain the ResultColumn DocumentPublishedReadOnlyDto and a separate publishedDto instance will be supplied + /// + /// + public static IContent BuildEntity(DocumentDto dto, IContentType contentType, DocumentPublishedReadOnlyDto publishedDto = null) { - var content = new Content(dto.Text, dto.ContentVersionDto.ContentDto.NodeDto.ParentId, _contentType); + var content = new Content(dto.Text, dto.ContentVersionDto.ContentDto.NodeDto.ParentId, contentType); try { content.DisableChangeTracking(); - content.Id = _id; + content.Id = dto.NodeId; content.Key = dto.ContentVersionDto.ContentDto.NodeDto.UniqueId; content.Name = dto.Text; content.NodeName = dto.ContentVersionDto.ContentDto.NodeDto.Text; @@ -49,10 +62,18 @@ namespace Umbraco.Core.Persistence.Factories content.Published = dto.Published; content.CreateDate = dto.ContentVersionDto.ContentDto.NodeDto.CreateDate; content.UpdateDate = dto.ContentVersionDto.VersionDate; - content.ExpireDate = dto.ExpiresDate.HasValue ? dto.ExpiresDate.Value : (DateTime?) null; - content.ReleaseDate = dto.ReleaseDate.HasValue ? dto.ReleaseDate.Value : (DateTime?) null; + content.ExpireDate = dto.ExpiresDate.HasValue ? dto.ExpiresDate.Value : (DateTime?)null; + content.ReleaseDate = dto.ReleaseDate.HasValue ? dto.ReleaseDate.Value : (DateTime?)null; content.Version = dto.ContentVersionDto.VersionId; - content.PublishedVersionGuid = dto.DocumentPublishedReadOnlyDto == null ? default(Guid) : dto.DocumentPublishedReadOnlyDto.VersionId; + + + //Check if the publishedDto has been supplied, if not the use the dto's own DocumentPublishedReadOnlyDto value + content.PublishedVersionGuid = publishedDto == null + ? (dto.DocumentPublishedReadOnlyDto == null ? default(Guid) : dto.DocumentPublishedReadOnlyDto.VersionId) + : publishedDto.VersionId; + content.PublishedDate = publishedDto == null + ? (dto.DocumentPublishedReadOnlyDto == null ? default(DateTime) : dto.DocumentPublishedReadOnlyDto.VersionDate) + : publishedDto.VersionDate; //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 @@ -63,6 +84,13 @@ namespace Umbraco.Core.Persistence.Factories { content.EnableChangeTracking(); } + + } + + [Obsolete("Use the static BuildEntity instead so we don't have to allocate one of these objects everytime we want to map values")] + public IContent BuildEntity(DocumentDto dto) + { + return BuildEntity(dto, _contentType); } public DocumentDto BuildDto(IContent entity) diff --git a/src/Umbraco.Core/Persistence/Factories/MacroFactory.cs b/src/Umbraco.Core/Persistence/Factories/MacroFactory.cs index 237bf1c521..c3acd7ff29 100644 --- a/src/Umbraco.Core/Persistence/Factories/MacroFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/MacroFactory.cs @@ -7,12 +7,9 @@ namespace Umbraco.Core.Persistence.Factories { internal class MacroFactory { - #region Implementation of IEntityFactory - public IMacro BuildEntity(MacroDto dto) { - var model = new Macro(dto.Id, dto.UseInEditor, dto.RefreshRate, dto.Alias, dto.Name, dto.ScriptType, dto.ScriptAssembly, dto.Xslt, dto.CacheByPage, dto.CachePersonalized, dto.DontRender, dto.MacroFilePath); - + var model = new Macro(dto.Id, dto.UniqueId, dto.UseInEditor, dto.RefreshRate, dto.Alias, dto.Name, dto.ScriptType, dto.ScriptAssembly, dto.Xslt, dto.CacheByPage, dto.CachePersonalized, dto.DontRender, dto.MacroFilePath); try { @@ -20,7 +17,7 @@ namespace Umbraco.Core.Persistence.Factories foreach (var p in dto.MacroPropertyDtos.EmptyNull()) { - model.Properties.Add(new MacroProperty(p.Id, p.Alias, p.Name, p.SortOrder, p.EditorAlias)); + model.Properties.Add(new MacroProperty(p.Id, p.UniqueId, p.Alias, p.Name, p.SortOrder, p.EditorAlias)); } //on initial construction we don't want to have dirty properties tracked @@ -36,8 +33,9 @@ namespace Umbraco.Core.Persistence.Factories public MacroDto BuildDto(IMacro entity) { - var dto = new MacroDto() - { + var dto = new MacroDto + { + UniqueId = entity.Key, Alias = entity.Alias, CacheByPage = entity.CacheByPage, CachePersonalized = entity.CacheByMember, @@ -58,8 +56,6 @@ namespace Umbraco.Core.Persistence.Factories return dto; } - #endregion - private List BuildPropertyDtos(IMacro entity) { var list = new List(); @@ -67,6 +63,7 @@ namespace Umbraco.Core.Persistence.Factories { var text = new MacroPropertyDto { + UniqueId = p.Key, Alias = p.Alias, Name = p.Name, Macro = entity.Id, diff --git a/src/Umbraco.Core/Persistence/Factories/MediaFactory.cs b/src/Umbraco.Core/Persistence/Factories/MediaFactory.cs index 0fcb654cb7..5729bb125e 100644 --- a/src/Umbraco.Core/Persistence/Factories/MediaFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/MediaFactory.cs @@ -27,15 +27,15 @@ namespace Umbraco.Core.Persistence.Factories #region Implementation of IEntityFactory - public IMedia BuildEntity(ContentVersionDto dto) + public static IMedia BuildEntity(ContentVersionDto dto, IMediaType contentType) { - var media = new Models.Media(dto.ContentDto.NodeDto.Text, dto.ContentDto.NodeDto.ParentId, _contentType); + var media = new Models.Media(dto.ContentDto.NodeDto.Text, dto.ContentDto.NodeDto.ParentId, contentType); try { media.DisableChangeTracking(); - media.Id = _id; + media.Id = dto.NodeId; media.Key = dto.ContentDto.NodeDto.UniqueId; media.Path = dto.ContentDto.NodeDto.Path; media.CreatorId = dto.ContentDto.NodeDto.UserId.Value; @@ -55,6 +55,13 @@ namespace Umbraco.Core.Persistence.Factories { media.EnableChangeTracking(); } + + } + + [Obsolete("Use the static BuildEntity instead so we don't have to allocate one of these objects everytime we want to map values")] + public IMedia BuildEntity(ContentVersionDto dto) + { + return BuildEntity(dto, _contentType); } public ContentVersionDto BuildDto(IMedia entity) diff --git a/src/Umbraco.Core/Persistence/Factories/MemberFactory.cs b/src/Umbraco.Core/Persistence/Factories/MemberFactory.cs index 2901f48539..7b28808429 100644 --- a/src/Umbraco.Core/Persistence/Factories/MemberFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/MemberFactory.cs @@ -28,17 +28,17 @@ namespace Umbraco.Core.Persistence.Factories #region Implementation of IEntityFactory - public IMember BuildEntity(MemberDto dto) + public static IMember BuildEntity(MemberDto dto, IMemberType contentType) { var member = new Member( dto.ContentVersionDto.ContentDto.NodeDto.Text, - dto.Email, dto.LoginName, dto.Password, _contentType); + dto.Email, dto.LoginName, dto.Password, contentType); try { member.DisableChangeTracking(); - member.Id = _id; + member.Id = dto.NodeId; member.Key = dto.ContentVersionDto.ContentDto.NodeDto.UniqueId; member.Path = dto.ContentVersionDto.ContentDto.NodeDto.Path; member.CreatorId = dto.ContentVersionDto.ContentDto.NodeDto.UserId.Value; @@ -62,6 +62,12 @@ namespace Umbraco.Core.Persistence.Factories } } + [Obsolete("Use the static BuildEntity instead so we don't have to allocate one of these objects everytime we want to map values")] + public IMember BuildEntity(MemberDto dto) + { + return BuildEntity(dto, _contentType); + } + public MemberDto BuildDto(IMember entity) { var dto = new MemberDto diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs index 446bd426ad..f202d8c321 100644 --- a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs @@ -30,11 +30,11 @@ namespace Umbraco.Core.Persistence.Factories _updateDate = updateDate; } - public IEnumerable BuildEntity(PropertyDataDto[] dtos) + public static IEnumerable BuildEntity(IReadOnlyCollection dtos, PropertyType[] compositionTypeProperties, DateTime createDate, DateTime updateDate) { var properties = new List(); - foreach (var propertyType in _compositionTypeProperties) + foreach (var propertyType in compositionTypeProperties) { var propertyDataDto = dtos.LastOrDefault(x => x.PropertyTypeId == propertyType.Id); var property = propertyDataDto == null @@ -47,8 +47,8 @@ namespace Umbraco.Core.Persistence.Factories //on initial construction we don't want to have dirty properties tracked property.DisableChangeTracking(); - property.CreateDate = _createDate; - property.UpdateDate = _updateDate; + property.CreateDate = createDate; + property.UpdateDate = updateDate; // http://issues.umbraco.org/issue/U4-1946 property.ResetDirtyProperties(false); properties.Add(property); @@ -57,12 +57,18 @@ namespace Umbraco.Core.Persistence.Factories { property.EnableChangeTracking(); } - + } return properties; } + [Obsolete("Use the static method instead, there's no reason to allocate one of these classes everytime we want to map values")] + public IEnumerable BuildEntity(PropertyDataDto[] dtos) + { + return BuildEntity(dtos, _compositionTypeProperties, _createDate, _updateDate); + } + public IEnumerable BuildDto(IEnumerable properties) { var propertyDataDtos = new List(); diff --git a/src/Umbraco.Core/Persistence/Factories/RelationTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/RelationTypeFactory.cs index 98d4f30042..b112535817 100644 --- a/src/Umbraco.Core/Persistence/Factories/RelationTypeFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/RelationTypeFactory.cs @@ -16,6 +16,7 @@ namespace Umbraco.Core.Persistence.Factories entity.DisableChangeTracking(); entity.Id = dto.Id; + entity.Key = dto.UniqueId; entity.IsBidirectional = dto.Dual; entity.Name = dto.Name; @@ -38,10 +39,13 @@ namespace Umbraco.Core.Persistence.Factories ChildObjectType = entity.ChildObjectType, Dual = entity.IsBidirectional, Name = entity.Name, - ParentObjectType = entity.ParentObjectType + ParentObjectType = entity.ParentObjectType, + UniqueId = entity.Key }; if (entity.HasIdentity) + { dto.Id = entity.Id; + } return dto; } diff --git a/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs b/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs index cf173dfa0c..ded7c60676 100644 --- a/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs @@ -18,7 +18,7 @@ namespace Umbraco.Core.Persistence.Factories //figure out what extra properties we have that are not on the IUmbracoEntity and add them to additional data foreach (var k in originalEntityProperties.Keys - .Select(x => new { orig = x, title = x.ToCleanString(CleanStringType.PascalCase) }) + .Select(x => new { orig = x, title = x.ToCleanString(CleanStringType.PascalCase | CleanStringType.Ascii | CleanStringType.ConvertCase) }) .Where(x => entityProps.InvariantContains(x.title) == false)) { entity.AdditionalData[k.title] = originalEntityProperties[k.orig]; @@ -51,7 +51,7 @@ namespace Umbraco.Core.Persistence.Factories entity.ContentTypeThumbnail = asDictionary.ContainsKey("thumbnail") ? (d.thumbnail ?? string.Empty) : string.Empty; var publishedVersion = default(Guid); - //some content items don't have a published version + //some content items don't have a published/newest version if (asDictionary.ContainsKey("publishedVersion") && asDictionary["publishedVersion"] != null) { Guid.TryParse(d.publishedVersion.ToString(), out publishedVersion); @@ -76,62 +76,6 @@ namespace Umbraco.Core.Persistence.Factories entity.EnableChangeTracking(); } } - - public UmbracoEntity BuildEntity(EntityRepository.UmbracoEntityDto dto) - { - var entity = new UmbracoEntity(dto.Trashed) - { - CreateDate = dto.CreateDate, - CreatorId = dto.UserId.Value, - Id = dto.NodeId, - Key = dto.UniqueId, - Level = dto.Level, - Name = dto.Text, - NodeObjectTypeId = dto.NodeObjectType.Value, - ParentId = dto.ParentId, - Path = dto.Path, - SortOrder = dto.SortOrder, - HasChildren = dto.Children > 0, - ContentTypeAlias = dto.Alias ?? string.Empty, - ContentTypeIcon = dto.Icon ?? string.Empty, - ContentTypeThumbnail = dto.Thumbnail ?? string.Empty, - }; - - entity.IsPublished = dto.PublishedVersion != default(Guid) || (dto.NewestVersion != default(Guid) && dto.PublishedVersion == dto.NewestVersion); - entity.IsDraft = dto.NewestVersion != default(Guid) && (dto.PublishedVersion == default(Guid) || dto.PublishedVersion != dto.NewestVersion); - entity.HasPendingChanges = (dto.PublishedVersion != default(Guid) && dto.NewestVersion != default(Guid)) && dto.PublishedVersion != dto.NewestVersion; - - foreach (var propertyDto in dto.UmbracoPropertyDtos.EmptyNull()) - { - entity.AdditionalData[propertyDto.PropertyAlias] = new UmbracoEntity.EntityProperty - { - PropertyEditorAlias = propertyDto.PropertyEditorAlias, - Value = propertyDto.NTextValue.IsNullOrWhiteSpace() - ? propertyDto.NVarcharValue - : propertyDto.NTextValue.ConvertToJsonIfPossible() - }; - } - - return entity; - } - - public EntityRepository.UmbracoEntityDto BuildDto(UmbracoEntity entity) - { - var node = new EntityRepository.UmbracoEntityDto - { - CreateDate = entity.CreateDate, - Level = short.Parse(entity.Level.ToString(CultureInfo.InvariantCulture)), - NodeId = entity.Id, - NodeObjectType = entity.NodeObjectTypeId, - ParentId = entity.ParentId, - Path = entity.Path, - SortOrder = entity.SortOrder, - Text = entity.Name, - Trashed = entity.Trashed, - UniqueId = entity.Key, - UserId = entity.CreatorId - }; - return node; - } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/IDatabaseScope.cs b/src/Umbraco.Core/Persistence/IDatabaseScope.cs deleted file mode 100644 index c9566b07a4..0000000000 --- a/src/Umbraco.Core/Persistence/IDatabaseScope.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Umbraco.Core.Persistence -{ - /// - /// Represents a database scope. - /// - public interface IDatabaseScope : IDisposable - { - IUmbracoDatabase Database { get; } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/IDatabaseScopeAccessor.cs b/src/Umbraco.Core/Persistence/IDatabaseScopeAccessor.cs deleted file mode 100644 index 2799114cc5..0000000000 --- a/src/Umbraco.Core/Persistence/IDatabaseScopeAccessor.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Umbraco.Core.Persistence -{ - /// - /// Provides access to DatabaseScope. - /// - public interface IDatabaseScopeAccessor - { - DatabaseScope Scope { get; set; } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/IUmbracoDatabase.cs b/src/Umbraco.Core/Persistence/IUmbracoDatabase.cs index 113e028f66..37977b4d56 100644 --- a/src/Umbraco.Core/Persistence/IUmbracoDatabase.cs +++ b/src/Umbraco.Core/Persistence/IUmbracoDatabase.cs @@ -21,5 +21,10 @@ namespace Umbraco.Core.Persistence /// 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; } } } diff --git a/src/Umbraco.Core/Persistence/IUmbracoDatabaseFactory.cs b/src/Umbraco.Core/Persistence/IUmbracoDatabaseFactory.cs index 8384ef9346..d9cb46333d 100644 --- a/src/Umbraco.Core/Persistence/IUmbracoDatabaseFactory.cs +++ b/src/Umbraco.Core/Persistence/IUmbracoDatabaseFactory.cs @@ -7,38 +7,12 @@ namespace Umbraco.Core.Persistence /// public interface IUmbracoDatabaseFactory : IDatabaseContext, IDisposable { - /// - /// Gets (creates if needed) the ambient database. - /// - /// There is no ambient database scope. - /// The ambient database is the database owned by the ambient scope. It should not - /// be disposed, as the scope takes care of disposing it when appropriate. - IUmbracoDatabase GetDatabase(); - - /// - /// Gets (creates if needed) the ambient database. - /// - /// This is just a shortcut to GetDatabase. - IUmbracoDatabase Database { get; } // fixme keep? - /// /// Creates a new database. /// - /// The new database is not part of any scope and must be disposed after being used. + /// The new database must be disposed after being used. IUmbracoDatabase CreateDatabase(); - /// - /// Creates a new database scope. - /// - /// A database for the scope. - /// - /// The new database scope becomes the ambient scope and may be nested under - /// an already existing ambient scope. - /// In most cases, should be null. It can be used to force the temporary - /// usage of another database instance. Use with care. - /// - IDatabaseScope CreateScope(IUmbracoDatabase database = null); - /// /// Gets a value indicating whether the database factory is configured. /// diff --git a/src/Umbraco.Core/Persistence/Mappers/BaseMapper.cs b/src/Umbraco.Core/Persistence/Mappers/BaseMapper.cs index 84d86c3dcd..e1b8bffd9f 100644 --- a/src/Umbraco.Core/Persistence/Mappers/BaseMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/BaseMapper.cs @@ -4,10 +4,11 @@ using System.Linq.Expressions; using System.Reflection; using NPoco; using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Plugins; namespace Umbraco.Core.Persistence.Mappers { - public abstract class BaseMapper + public abstract class BaseMapper : IDiscoverable { protected BaseMapper() { diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs index 9756a30b64..f89bb6c795 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs @@ -131,21 +131,21 @@ namespace Umbraco.Core.Persistence.Migrations.Initial _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = Constants.DataTypes.DefaultMembersListView, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,-97", SortOrder = 2, UniqueId = new Guid("AA2C52A0-CE87-4E65-A47C-7DF09358585D"), Text = Constants.Conventions.DataTypes.ListViewPrefix + "Members", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1031, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1031", SortOrder = 2, UniqueId = new Guid("f38bd2d7-65d0-48e6-95dc-87ce06ec2d3d"), Text = Constants.Conventions.MediaTypes.Folder, NodeObjectType = new Guid(Constants.ObjectTypes.MediaType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1032, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1032", SortOrder = 2, UniqueId = new Guid("cc07b313-0843-4aa8-bbda-871c8da728c8"), Text = Constants.Conventions.MediaTypes.Image, NodeObjectType = new Guid(Constants.ObjectTypes.MediaType), CreateDate = DateTime.Now }); - _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1033, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1033", SortOrder = 2, UniqueId = new Guid("4c52d8ab-54e6-40cd-999c-7a5f24903e4d"), Text = Constants.Conventions.MediaTypes.File, NodeObjectType = new Guid(Constants.ObjectTypes.MediaType), CreateDate = DateTime.Now }); - _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1034, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1034", SortOrder = 2, UniqueId = new Guid("a6857c73-d6e9-480c-b6e6-f15f6ad11125"), Text = "Content Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); - _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1035, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1035", SortOrder = 2, UniqueId = new Guid("93929b9a-93a2-4e2a-b239-d99334440a59"), Text = "Media Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); - _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1036, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1036", SortOrder = 2, UniqueId = new Guid("2b24165f-9782-4aa3-b459-1de4a4d21f60"), Text = "Member Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); - _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1040, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1040", SortOrder = 2, UniqueId = new Guid("21e798da-e06e-4eda-a511-ed257f78d4fa"), Text = "Related Links", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); + _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1033, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1033", SortOrder = 2, UniqueId = new Guid("4c52d8ab-54e6-40cd-999c-7a5f24903e4d"), Text = Constants.Conventions.MediaTypes.File, NodeObjectType = new Guid(Constants.ObjectTypes.MediaType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1041, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1041", SortOrder = 2, UniqueId = new Guid("b6b73142-b9c1-4bf8-a16d-e1c23320b549"), Text = "Tags", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1043, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1043", SortOrder = 2, UniqueId = new Guid("1df9f033-e6d4-451f-b8d2-e0cbc50a836f"), Text = "Image Cropper", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1044, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1044", SortOrder = 0, UniqueId = new Guid("d59be02f-1df9-4228-aa1e-01917d806cda"), Text = Constants.Conventions.MemberTypes.DefaultAlias, NodeObjectType = new Guid(Constants.ObjectTypes.MemberType), CreateDate = DateTime.Now }); - _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1045, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1045", SortOrder = 2, UniqueId = new Guid("7E3962CC-CE20-4FFC-B661-5897A894BA7E"), Text = "Multiple Media Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); + //New UDI pickers with newer Ids + _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1046, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1046", SortOrder = 2, UniqueId = new Guid("FD1E0DA5-5606-4862-B679-5D0CF3A52A59"), Text = "Content Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); + _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1047, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1047", SortOrder = 2, UniqueId = new Guid("1EA2E01F-EBD8-4CE1-8D71-6B1149E63548"), Text = "Member Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); + _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1048, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1048", SortOrder = 2, UniqueId = new Guid("135D60E0-64D9-49ED-AB08-893C9BA44AE5"), Text = "Media Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); + _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1049, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1049", SortOrder = 2, UniqueId = new Guid("9DBBCBBB-2327-434A-B355-AF1B84E5010A"), Text = "Multiple Media Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); + _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1050, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1050", SortOrder = 2, UniqueId = new Guid("B4E3535A-1753-47E2-8568-602CF8CFEE6F"), Text = "Related Links", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); //TODO: We're not creating these for 7.0 //_database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1039, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1039", SortOrder = 2, UniqueId = new Guid("06f349a9-c949-4b6a-8660-59c10451af42"), Text = "Ultimate Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); //_database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1038, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1038", SortOrder = 2, UniqueId = new Guid("1251c96c-185c-4e9b-93f4-b48205573cbd"), Text = "Simple Editor", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); - //_database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1042, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1042", SortOrder = 2, UniqueId = new Guid("0a452bd5-83f9-4bc3-8403-1286e13fb77e"), Text = "Macro Container", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); } @@ -255,18 +255,20 @@ namespace Umbraco.Core.Persistence.Migrations.Initial _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 12, DataTypeId = -40, PropertyEditorAlias = Constants.PropertyEditors.RadioButtonListAlias, DbType = "Nvarchar" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 13, DataTypeId = -41, PropertyEditorAlias = Constants.PropertyEditors.DateAlias, DbType = "Date" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 14, DataTypeId = -42, PropertyEditorAlias = Constants.PropertyEditors.DropDownListAlias, DbType = "Integer" }); - _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 15, DataTypeId = -43, PropertyEditorAlias = Constants.PropertyEditors.CheckBoxListAlias, DbType = "Nvarchar" }); - _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 16, DataTypeId = 1034, PropertyEditorAlias = Constants.PropertyEditors.ContentPickerAlias, DbType = "Integer" }); - _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 18, DataTypeId = 1036, PropertyEditorAlias = Constants.PropertyEditors.MemberPickerAlias, DbType = "Integer" }); - _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 17, DataTypeId = 1035, PropertyEditorAlias = Constants.PropertyEditors.MultipleMediaPickerAlias, DbType = "Nvarchar" }); - _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 21, DataTypeId = 1040, PropertyEditorAlias = Constants.PropertyEditors.RelatedLinksAlias, DbType = "Ntext" }); + _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 15, DataTypeId = -43, PropertyEditorAlias = Constants.PropertyEditors.CheckBoxListAlias, DbType = "Nvarchar" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 22, DataTypeId = 1041, PropertyEditorAlias = Constants.PropertyEditors.TagsAlias, DbType = "Ntext" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 24, DataTypeId = 1043, PropertyEditorAlias = Constants.PropertyEditors.ImageCropperAlias, DbType = "Ntext" }); - _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 25, DataTypeId = 1045, PropertyEditorAlias = Constants.PropertyEditors.MultipleMediaPickerAlias, DbType = "Nvarchar" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = -26, DataTypeId = Constants.DataTypes.DefaultContentListView, PropertyEditorAlias = Constants.PropertyEditors.ListViewAlias, DbType = "Nvarchar" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = -27, DataTypeId = Constants.DataTypes.DefaultMediaListView, PropertyEditorAlias = Constants.PropertyEditors.ListViewAlias, DbType = "Nvarchar" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = -28, DataTypeId = Constants.DataTypes.DefaultMembersListView, PropertyEditorAlias = Constants.PropertyEditors.ListViewAlias, DbType = "Nvarchar" }); + //New UDI pickers with newer Ids + _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 26, DataTypeId = 1046, PropertyEditorAlias = Constants.PropertyEditors.ContentPicker2Alias, DbType = "Nvarchar" }); + _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 27, DataTypeId = 1047, PropertyEditorAlias = Constants.PropertyEditors.MemberPicker2Alias, DbType = "Nvarchar" }); + _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 28, DataTypeId = 1048, PropertyEditorAlias = Constants.PropertyEditors.MediaPicker2Alias, DbType = "Ntext" }); + _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 29, DataTypeId = 1049, PropertyEditorAlias = Constants.PropertyEditors.MediaPicker2Alias, DbType = "Ntext" }); + _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 30, DataTypeId = 1050, PropertyEditorAlias = Constants.PropertyEditors.RelatedLinks2Alias, DbType = "Ntext" }); + //TODO: We're not creating these for 7.0 //_database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 19, DataTypeId = 1038, PropertyEditorAlias = Constants.PropertyEditors.MarkdownEditorAlias, DbType = "Ntext" }); //_database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 20, DataTypeId = 1039, PropertyEditorAlias = Constants.PropertyEditors.UltimatePickerAlias, DbType = "Ntext" }); @@ -277,10 +279,7 @@ namespace Umbraco.Core.Persistence.Migrations.Initial { _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = 3, Alias = "", SortOrder = 0, DataTypeNodeId = -87, 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|" }); _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = 4, Alias = "group", SortOrder = 0, DataTypeNodeId = 1041, Value = "default" }); - - //default's for MultipleMediaPickerAlias picker - _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = 5, Alias = "multiPicker", SortOrder = 0, DataTypeNodeId = 1045, Value = "1" }); - + //defaults for the member list _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -1, Alias = "pageSize", SortOrder = 1, DataTypeNodeId = Constants.DataTypes.DefaultMembersListView, Value = "10" }); _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -2, Alias = "orderBy", SortOrder = 2, DataTypeNodeId = Constants.DataTypes.DefaultMembersListView, Value = "username" }); @@ -297,12 +296,19 @@ namespace Umbraco.Core.Persistence.Migrations.Initial _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -7, Alias = "orderDirection", SortOrder = 3, DataTypeNodeId = Constants.DataTypes.DefaultMediaListView, Value = "desc" }); _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -8, Alias = "layouts", SortOrder = 4, DataTypeNodeId = Constants.DataTypes.DefaultMediaListView, Value = "[" + cardLayout + "," + listLayout + "]" }); _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -9, Alias = "includeProperties", SortOrder = 5, DataTypeNodeId = Constants.DataTypes.DefaultMediaListView, Value = "[{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1},{\"alias\":\"owner\",\"header\":\"Updated by\",\"isSystem\":1}]" }); + + //default's for MultipleMediaPickerAlias picker + _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = 6, Alias = "multiPicker", SortOrder = 0, DataTypeNodeId = 1049, Value = "1" }); } private void CreateUmbracoRelationTypeData() { - _database.Insert("umbracoRelationType", "id", false, new RelationTypeDto { Id = 1, Alias = Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, ChildObjectType = new Guid(Constants.ObjectTypes.Document), ParentObjectType = new Guid(Constants.ObjectTypes.Document), Dual = true, Name = Constants.Conventions.RelationTypes.RelateDocumentOnCopyName }); - _database.Insert("umbracoRelationType", "id", false, new RelationTypeDto { Id = 2, Alias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias, ChildObjectType = new Guid(Constants.ObjectTypes.Document), ParentObjectType = new Guid(Constants.ObjectTypes.Document), Dual = false, Name = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName }); + var relationType = new RelationTypeDto { Id = 1, Alias = Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, ChildObjectType = new Guid(Constants.ObjectTypes.Document), ParentObjectType = new Guid(Constants.ObjectTypes.Document), Dual = true, Name = Constants.Conventions.RelationTypes.RelateDocumentOnCopyName }; + relationType.UniqueId = (relationType.Alias + "____" + relationType.Name).ToGuid(); + _database.Insert("umbracoRelationType", "id", false, relationType); + relationType = new RelationTypeDto { Id = 2, Alias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias, ChildObjectType = new Guid(Constants.ObjectTypes.Document), ParentObjectType = new Guid(Constants.ObjectTypes.Document), Dual = false, Name = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName }; + relationType.UniqueId = (relationType.Alias + "____" + relationType.Name).ToGuid(); + _database.Insert("umbracoRelationType", "id", false, relationType); } private void CreateCmsTaskTypeData() @@ -315,7 +321,7 @@ namespace Umbraco.Core.Persistence.Migrations.Initial var dto = new MigrationDto { Id = 1, - Name = GlobalSettings.UmbracoMigrationName, + Name = Constants.System.UmbracoMigrationName, Version = UmbracoVersion.SemanticVersion.ToString(), CreateDate = DateTime.Now }; diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs index 58cc7a2c0e..1a67b0e52e 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs @@ -13,7 +13,7 @@ namespace Umbraco.Core.Persistence.Migrations.Initial /// /// Represents the initial database schema creation by running CreateTable for all DTOs against the db. /// - internal class DatabaseSchemaCreation + public class DatabaseSchemaCreation { private readonly DatabaseSchemaHelper _schemaHelper; private readonly IUmbracoDatabase _database; @@ -47,6 +47,7 @@ namespace Umbraco.Core.Persistence.Migrations.Initial {7, typeof (DataTypeDto)}, {8, typeof (DataTypePreValueDto)}, {9, typeof (DictionaryDto)}, + {10, typeof (LanguageDto)}, {11, typeof (LanguageTextDto)}, {12, typeof (DomainDto)}, @@ -57,15 +58,19 @@ namespace Umbraco.Core.Persistence.Migrations.Initial {17, typeof (MemberDto)}, {18, typeof (Member2MemberGroupDto)}, {19, typeof (ContentXmlDto)}, + {20, typeof (PreviewXmlDto)}, {21, typeof (PropertyTypeGroupDto)}, {22, typeof (PropertyTypeDto)}, {23, typeof (PropertyDataDto)}, {24, typeof (RelationTypeDto)}, {25, typeof (RelationDto)}, - + //removed: {26... + //removed: {27... {28, typeof (TagDto)}, {29, typeof (TagRelationshipDto)}, + + //removed: {30... {31, typeof (UserTypeDto)}, {32, typeof (UserDto)}, {33, typeof (TaskTypeDto)}, @@ -75,16 +80,18 @@ namespace Umbraco.Core.Persistence.Migrations.Initial {37, typeof (User2AppDto)}, {38, typeof (User2NodeNotifyDto)}, {39, typeof (User2NodePermissionDto)}, + {40, typeof (ServerRegistrationDto)}, {41, typeof (AccessDto)}, {42, typeof (AccessRuleDto)}, {43, typeof (CacheInstructionDto)}, {44, typeof (ExternalLoginDto)}, {45, typeof (MigrationDto)}, - {46, typeof (UmbracoDeployChecksumDto)}, - {47, typeof (UmbracoDeployDependencyDto)}, + //removed: {46, typeof (UmbracoDeployChecksumDto)}, + //removed: {47, typeof (UmbracoDeployDependencyDto)}, {48, typeof (RedirectUrlDto) }, {49, typeof (LockDto) }, + {50, typeof (ContentNuDto) } }; #endregion @@ -338,12 +345,9 @@ namespace Umbraco.Core.Persistence.Migrations.Initial /// Raises the event. /// /// The instance containing the event data. - protected internal virtual void FireBeforeCreation(DatabaseCreationEventArgs e) + internal virtual void FireBeforeCreation(DatabaseCreationEventArgs e) { - if (BeforeCreation != null) - { - BeforeCreation(e); - } + BeforeCreation?.Invoke(e); } /// @@ -354,12 +358,9 @@ namespace Umbraco.Core.Persistence.Migrations.Initial /// Raises the event. /// /// The instance containing the event data. - protected virtual void FireAfterCreation(DatabaseCreationEventArgs e) + internal virtual void FireAfterCreation(DatabaseCreationEventArgs e) { - if (AfterCreation != null) - { - AfterCreation(e); - } + AfterCreation?.Invoke(e); } #endregion diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs index 5b980b6c23..1090a4302a 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs @@ -50,7 +50,7 @@ namespace Umbraco.Core.Persistence.Migrations.Initial if (ValidTables.Any(x => x.InvariantEquals("umbracoMigration"))) { - var allMigrations = migrationEntryService.GetAll(GlobalSettings.UmbracoMigrationName); + var allMigrations = migrationEntryService.GetAll(Constants.System.UmbracoMigrationName); mostrecent = allMigrations.OrderByDescending(x => x.Version).Select(x => x.Version).FirstOrDefault(); } @@ -66,6 +66,8 @@ namespace Umbraco.Core.Persistence.Migrations.Initial /// public Version DetermineInstalledVersion() { + // v8 = kill versions older than 7 + //If (ValidTables.Count == 0) database is empty and we return -> new Version(0, 0, 0); if (ValidTables.Count == 0) return new Version(0, 0, 0); @@ -139,6 +141,12 @@ namespace Umbraco.Core.Persistence.Migrations.Initial return new Version(7, 4, 0); } + //if the error indicates a problem with the column cmsMacroProperty.uniquePropertyId then it is not version 7.6 since that is when it is added + if (Errors.Any(x => x.Item1.Equals("Column") && (x.Item2.InvariantEquals("cmsMacroProperty,uniquePropertyId")))) + { + return new Version(7, 5, 0); + } + return UmbracoVersion.Current; } diff --git a/src/Umbraco.Core/Persistence/Migrations/Syntax/Create/CreateBuilder.cs b/src/Umbraco.Core/Persistence/Migrations/Syntax/Create/CreateBuilder.cs index c00225a73b..9af4c19d98 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Syntax/Create/CreateBuilder.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Syntax/Create/CreateBuilder.cs @@ -7,6 +7,7 @@ using Umbraco.Core.Persistence.Migrations.Syntax.Create.Expressions; using Umbraco.Core.Persistence.Migrations.Syntax.Create.ForeignKey; using Umbraco.Core.Persistence.Migrations.Syntax.Create.Index; using Umbraco.Core.Persistence.Migrations.Syntax.Create.Table; +using Umbraco.Core.Persistence.Migrations.Syntax.Execute.Expressions; using Umbraco.Core.Persistence.Migrations.Syntax.Expressions; using Umbraco.Core.Persistence.SqlSyntax; @@ -19,11 +20,30 @@ namespace Umbraco.Core.Persistence.Migrations.Syntax.Create public CreateBuilder(IMigrationContext context, params DatabaseType[] supportedDatabaseTypes) { - if (context == null) throw new ArgumentNullException(nameof(context)); - _context = context; + _context = context ?? throw new ArgumentNullException(nameof(context)); _supportedDatabaseTypes = supportedDatabaseTypes; } + private ISqlSyntaxProvider SqlSyntax => _context.Database.SqlSyntax; + + public void Table() + { + var tableDefinition = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); + + AddSql(SqlSyntax.Format(tableDefinition)); + AddSql(SqlSyntax.FormatPrimaryKey(tableDefinition)); + foreach (var sql in SqlSyntax.Format(tableDefinition.ForeignKeys)) + AddSql(sql); + foreach (var sql in SqlSyntax.Format(tableDefinition.Indexes)) + AddSql(sql); + } + + private void AddSql(string sql) + { + var expression = new ExecuteSqlStatementExpression(_context, _supportedDatabaseTypes) { SqlStatement = sql }; + _context.Expressions.Add(expression); + } + public ICreateTableWithColumnSyntax Table(string tableName) { var expression = new CreateTableExpression(_context, _supportedDatabaseTypes) { TableName = tableName }; diff --git a/src/Umbraco.Core/Persistence/Migrations/Syntax/Create/ICreateBuilder.cs b/src/Umbraco.Core/Persistence/Migrations/Syntax/Create/ICreateBuilder.cs index e079127705..3cb2fe7e9f 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Syntax/Create/ICreateBuilder.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Syntax/Create/ICreateBuilder.cs @@ -8,6 +8,8 @@ namespace Umbraco.Core.Persistence.Migrations.Syntax.Create { public interface ICreateBuilder : IFluentSyntax { + void Table(); + ICreateTableWithColumnSyntax Table(string tableName); ICreateColumnOnTableSyntax Column(string columnName); diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddContentNuTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddContentNuTable.cs index bcd1b98f27..9dadaa726b 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddContentNuTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddContentNuTable.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionEight { - [Migration("8.0.0", 100, GlobalSettings.UmbracoMigrationName)] + [Migration("8.0.0", 100, Constants.System.UmbracoMigrationName)] class AddContentNuTable : MigrationBase { public AddContentNuTable(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddLockObjects.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddLockObjects.cs index a82112561b..18fec935c9 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddLockObjects.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddLockObjects.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Models.Rdbms; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionEight { - [Migration("8.0.0", 101, GlobalSettings.UmbracoMigrationName)] + [Migration("8.0.0", 101, Constants.System.UmbracoMigrationName)] public class AddLockObjects : MigrationBase { public AddLockObjects(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddLockTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddLockTable.cs index afb39f6936..440f07d905 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddLockTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddLockTable.cs @@ -3,7 +3,7 @@ using Umbraco.Core.Configuration; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionEight { - [Migration("8.0.0", 100, GlobalSettings.UmbracoMigrationName)] + [Migration("8.0.0", 100, Constants.System.UmbracoMigrationName)] public class AddLockTable : MigrationBase { public AddLockTable(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddRedirectUrlTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddRedirectUrlTable.cs index 42c9f7a276..f917c38bf6 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddRedirectUrlTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddRedirectUrlTable.cs @@ -3,7 +3,7 @@ using Umbraco.Core.Configuration; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionEight { - [Migration("8.0.0", 100, GlobalSettings.UmbracoMigrationName)] + [Migration("8.0.0", 100, Constants.System.UmbracoMigrationName)] public class AddRedirectUrlTable : MigrationBase { public AddRedirectUrlTable(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/RefactorXmlColumns.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/RefactorXmlColumns.cs index 55b1c9ac47..a460ab500d 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/RefactorXmlColumns.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/RefactorXmlColumns.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Logging; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionEight { - [Migration("8.0.0", 100, GlobalSettings.UmbracoMigrationName)] + [Migration("8.0.0", 100, Constants.System.UmbracoMigrationName)] public class RefactorXmlColumns : MigrationBase { public RefactorXmlColumns(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionFourNineZero/RemoveUmbracoAppConstraints.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionFourNineZero/RemoveUmbracoAppConstraints.cs index c047c54d0d..3732cef74b 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionFourNineZero/RemoveUmbracoAppConstraints.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionFourNineZero/RemoveUmbracoAppConstraints.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionFourNineZero { - [MigrationAttribute("4.9.0", 0, GlobalSettings.UmbracoMigrationName)] + [MigrationAttribute("4.9.0", 0, Constants.System.UmbracoMigrationName)] public class RemoveUmbracoAppConstraints : MigrationBase { public RemoveUmbracoAppConstraints(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionFourOneZero/AddPreviewXmlTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionFourOneZero/AddPreviewXmlTable.cs index b6ac23e26a..991d136867 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionFourOneZero/AddPreviewXmlTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionFourOneZero/AddPreviewXmlTable.cs @@ -4,7 +4,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionFourOneZero { - [Migration("4.1.0", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("4.1.0", 0, Constants.System.UmbracoMigrationName)] public class AddPreviewXmlTable : MigrationBase { public AddPreviewXmlTable(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AddIndexToCmsMacroPropertyTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AddIndexToCmsMacroPropertyTable.cs index e1f1f460a0..358149a729 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AddIndexToCmsMacroPropertyTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AddIndexToCmsMacroPropertyTable.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven /// /// Creats a unique index across two columns so we cannot have duplicate property aliases for one macro /// - [Migration("7.0.0", 5, GlobalSettings.UmbracoMigrationName)] + [Migration("7.0.0", 5, Constants.System.UmbracoMigrationName)] public class AddIndexToCmsMacroPropertyTable : MigrationBase { private readonly bool _skipIndexCheck; diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AddIndexToCmsMacroTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AddIndexToCmsMacroTable.cs index 4bc54f47ad..0fc251dca6 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AddIndexToCmsMacroTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AddIndexToCmsMacroTable.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven /// /// Creates a unique index on the macro alias so we cannot have duplicates by alias /// - [Migration("7.0.0", 4, GlobalSettings.UmbracoMigrationName)] + [Migration("7.0.0", 4, Constants.System.UmbracoMigrationName)] public class AddIndexToCmsMacroTable : MigrationBase { private readonly bool _forTesting; diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AddPropertyEditorAliasColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AddPropertyEditorAliasColumn.cs index 074d0d7c2f..42586ad342 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AddPropertyEditorAliasColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AddPropertyEditorAliasColumn.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven { - [Migration("7.0.0", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("7.0.0", 0, Constants.System.UmbracoMigrationName)] public class AddPropertyEditorAliasColumn : MigrationBase { public AddPropertyEditorAliasColumn(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterCmsMacroPropertyTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterCmsMacroPropertyTable.cs index 747d29d6cf..ce94e5415d 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterCmsMacroPropertyTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterCmsMacroPropertyTable.cs @@ -8,6 +8,7 @@ using Umbraco.Core.Persistence.Migrations.Syntax.Execute; using Umbraco.Core.Persistence.Migrations.Syntax.Execute.Expressions; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Scoping; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven { @@ -16,14 +17,13 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven /// needs to be changed to editorAlias, we'll do this by removing the constraint,changing the macroPropertyType to the new /// editorAlias column (and maintaing data so we can reference it) /// - [Migration("7.0.0", 6, GlobalSettings.UmbracoMigrationName)] + [Migration("7.0.0", 6, Constants.System.UmbracoMigrationName)] public class AlterCmsMacroPropertyTable : MigrationBase { public AlterCmsMacroPropertyTable(IMigrationContext context) : base(context) { } - public override void Up() { //now that the controlId column is renamed and now a string we need to convert @@ -41,7 +41,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven //If we are on SQLServer, we need to delete default constraints by name, older versions of umbraco did not name these default constraints // consistently so we need to look up the constraint name to delete, this only pertains to SQL Server and this issue: // http://issues.umbraco.org/issue/U4-4133 - var sqlServerSyntaxProvider = new SqlServerSyntaxProvider(new Lazy(() => null)); + var sqlServerSyntaxProvider = new SqlServerSyntaxProvider(new Lazy(() => null)); var defaultConstraints = sqlServerSyntaxProvider.GetDefaultConstraintsPerColumn(Context.Database).Distinct(); //lookup the constraint we want to delete, normally would be called "DF_cmsMacroProperty_macroPropertyHidden" but diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagRelationsTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagRelationsTable.cs index 2f61ae0f99..f4bc5b0a36 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagRelationsTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagRelationsTable.cs @@ -9,7 +9,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven { - [Migration("7.0.0", 8, GlobalSettings.UmbracoMigrationName)] + [Migration("7.0.0", 8, Constants.System.UmbracoMigrationName)] public class AlterTagRelationsTable : MigrationBase { public AlterTagRelationsTable(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagsTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagsTable.cs index 1c61e91072..a3c3d9fde5 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagsTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagsTable.cs @@ -7,7 +7,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven { - [Migration("7.0.0", 9, GlobalSettings.UmbracoMigrationName)] + [Migration("7.0.0", 9, Constants.System.UmbracoMigrationName)] public class AlterTagsTable : MigrationBase { public AlterTagsTable(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterUserTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterUserTable.cs index c6c88cba09..7b95ab4be2 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterUserTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterUserTable.cs @@ -4,7 +4,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven { - [Migration("7.0.0", 3, GlobalSettings.UmbracoMigrationName)] + [Migration("7.0.0", 3, Constants.System.UmbracoMigrationName)] public class AlterUserTable : MigrationBase { public AlterUserTable(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AssignMissingKeysAndIndexes.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AssignMissingKeysAndIndexes.cs index 6b9dd6f43b..8fe4800337 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AssignMissingKeysAndIndexes.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AssignMissingKeysAndIndexes.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven /// and it wasn't a MySQL install. /// see: http://issues.umbraco.org/issue/U4-5707 /// - [Migration("7.0.0", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("7.0.0", 0, Constants.System.UmbracoMigrationName)] public class AssignMissingKeysAndIndexes : MigrationBase { public AssignMissingKeysAndIndexes(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/DropControlIdColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/DropControlIdColumn.cs index 86269c2a82..f51289ae1b 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/DropControlIdColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/DropControlIdColumn.cs @@ -4,7 +4,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven { - [Migration("7.0.0", 2, GlobalSettings.UmbracoMigrationName)] + [Migration("7.0.0", 2, Constants.System.UmbracoMigrationName)] public class DropControlIdColumn : MigrationBase { public DropControlIdColumn(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/RemoveCmsMacroPropertyTypeTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/RemoveCmsMacroPropertyTypeTable.cs index 3eba55b4e7..37a34cf7e2 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/RemoveCmsMacroPropertyTypeTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/RemoveCmsMacroPropertyTypeTable.cs @@ -4,7 +4,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven { - [Migration("7.0.0", 7, GlobalSettings.UmbracoMigrationName)] + [Migration("7.0.0", 7, Constants.System.UmbracoMigrationName)] public class RemoveCmsMacroPropertyTypeTable : MigrationBase { public RemoveCmsMacroPropertyTypeTable(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/UpdateControlIdToPropertyEditorAlias.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/UpdateControlIdToPropertyEditorAlias.cs index d4616ea199..c6eff6cf55 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/UpdateControlIdToPropertyEditorAlias.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/UpdateControlIdToPropertyEditorAlias.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven /// /// Updates the data in the changed propertyEditorAlias column after it has been changed by ChangeControlIdColumn /// - [Migration("7.0.0", 1, GlobalSettings.UmbracoMigrationName)] + [Migration("7.0.0", 1, Constants.System.UmbracoMigrationName)] public class UpdateControlIdToPropertyEditorAlias : MigrationBase { public UpdateControlIdToPropertyEditorAlias(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/UpdateRelatedLinksData.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/UpdateRelatedLinksData.cs index 8baf88c451..e6a5888552 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/UpdateRelatedLinksData.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/UpdateRelatedLinksData.cs @@ -17,7 +17,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven { - [Migration("7.0.0", 10, GlobalSettings.UmbracoMigrationName)] + [Migration("7.0.0", 10, Constants.System.UmbracoMigrationName)] public class UpdateRelatedLinksData : MigrationBase { public UpdateRelatedLinksData(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveFive/AddLockObjects.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveFive/AddLockObjects.cs index 8ee31f4b07..9240e2e86e 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveFive/AddLockObjects.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveFive/AddLockObjects.cs @@ -3,7 +3,7 @@ using Umbraco.Core.Models.Rdbms; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFiveFive { - [Migration("7.5.5", 101, GlobalSettings.UmbracoMigrationName)] + [Migration("7.5.5", 101, Constants.System.UmbracoMigrationName)] public class AddLockObjects : MigrationBase { public AddLockObjects(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveFive/AddLockTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveFive/AddLockTable.cs index 8d7f2ed8fb..17f7ee9df4 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveFive/AddLockTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveFive/AddLockTable.cs @@ -3,7 +3,7 @@ using Umbraco.Core.Configuration; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFiveFive { - [Migration("7.5.5", 100, GlobalSettings.UmbracoMigrationName)] + [Migration("7.5.5", 100, Constants.System.UmbracoMigrationName)] public class AddLockTable : MigrationBase { public AddLockTable(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveFive/UpdateAllowedMediaTypesAtRoot.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveFive/UpdateAllowedMediaTypesAtRoot.cs index 5a95b9b8bf..8f2c1586eb 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveFive/UpdateAllowedMediaTypesAtRoot.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveFive/UpdateAllowedMediaTypesAtRoot.cs @@ -7,7 +7,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFiveFiv /// /// See: http://issues.umbraco.org/issue/U4-4196 /// - [Migration("7.5.5", 1, GlobalSettings.UmbracoMigrationName)] + [Migration("7.5.5", 1, Constants.System.UmbracoMigrationName)] public class UpdateAllowedMediaTypesAtRoot : MigrationBase { public UpdateAllowedMediaTypesAtRoot(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/RemoveStylesheetDataAndTablesAgain.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/RemoveStylesheetDataAndTablesAgain.cs index f0365b3578..091c44c1a9 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/RemoveStylesheetDataAndTablesAgain.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/RemoveStylesheetDataAndTablesAgain.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFiveZer /// /// This is here to re-remove these tables, we dropped them in 7.3 but new installs created them again so we're going to re-drop them /// - [Migration("7.5.0", 1, GlobalSettings.UmbracoMigrationName)] + [Migration("7.5.0", 1, Constants.System.UmbracoMigrationName)] public class RemoveStylesheetDataAndTablesAgain : MigrationBase { public RemoveStylesheetDataAndTablesAgain(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/UpdateUniqueIndexOnCmsPropertyData.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/UpdateUniqueIndexOnCmsPropertyData.cs index a9788ef3ae..ed6f6b73f2 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/UpdateUniqueIndexOnCmsPropertyData.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/UpdateUniqueIndexOnCmsPropertyData.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFiveZer /// /// See: http://issues.umbraco.org/issue/U4-8522 /// - [Migration("7.5.0", 2, GlobalSettings.UmbracoMigrationName)] + [Migration("7.5.0", 2, Constants.System.UmbracoMigrationName)] public class UpdateUniqueIndexOnCmsPropertyData : MigrationBase { public UpdateUniqueIndexOnCmsPropertyData(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddDataDecimalColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddDataDecimalColumn.cs index 8098e44087..d40efd8633 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddDataDecimalColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddDataDecimalColumn.cs @@ -4,7 +4,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFourZero { - [Migration("7.4.0", 1, GlobalSettings.UmbracoMigrationName)] + [Migration("7.4.0", 1, Constants.System.UmbracoMigrationName)] public class AddDataDecimalColumn : MigrationBase { public AddDataDecimalColumn(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddUmbracoDeployTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddUmbracoDeployTables.cs index c6186ce75a..42224f0609 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddUmbracoDeployTables.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddUmbracoDeployTables.cs @@ -4,7 +4,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFourZero { - [Migration("7.4.0", 5, GlobalSettings.UmbracoMigrationName)] + [Migration("7.4.0", 5, Constants.System.UmbracoMigrationName)] public class AddUmbracoDeployTables : MigrationBase { public AddUmbracoDeployTables(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddUniqueIdPropertyTypeGroupColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddUniqueIdPropertyTypeGroupColumn.cs index 4537eb8523..137e56158f 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddUniqueIdPropertyTypeGroupColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddUniqueIdPropertyTypeGroupColumn.cs @@ -9,7 +9,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFourZero { - [Migration("7.4.0", 2, GlobalSettings.UmbracoMigrationName)] + [Migration("7.4.0", 2, Constants.System.UmbracoMigrationName)] public class AddUniqueIdPropertyTypeGroupColumn : MigrationBase { public AddUniqueIdPropertyTypeGroupColumn(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/EnsureContentTypeUniqueIdsAreConsistent.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/EnsureContentTypeUniqueIdsAreConsistent.cs index 107560a3f2..c9d14f31f9 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/EnsureContentTypeUniqueIdsAreConsistent.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/EnsureContentTypeUniqueIdsAreConsistent.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFourZer /// alias, so we need to ensure that these are initially consistent on /// all environments (based on the alias). /// - [Migration("7.4.0", 3, GlobalSettings.UmbracoMigrationName)] + [Migration("7.4.0", 3, Constants.System.UmbracoMigrationName)] public class EnsureContentTypeUniqueIdsAreConsistent : MigrationBase { public EnsureContentTypeUniqueIdsAreConsistent(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/FixListViewMediaSortOrder.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/FixListViewMediaSortOrder.cs index 36cd45b0a7..7d5195331f 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/FixListViewMediaSortOrder.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/FixListViewMediaSortOrder.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFourZero { - [Migration("7.4.0", 4, GlobalSettings.UmbracoMigrationName)] + [Migration("7.4.0", 4, Constants.System.UmbracoMigrationName)] public class FixListViewMediaSortOrder : MigrationBase { public FixListViewMediaSortOrder(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/RemoveParentIdPropertyTypeGroupColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/RemoveParentIdPropertyTypeGroupColumn.cs index 4d6fdfe479..e0c9184905 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/RemoveParentIdPropertyTypeGroupColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/RemoveParentIdPropertyTypeGroupColumn.cs @@ -4,7 +4,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFourZero { - [Migration("7.4.0", 4, GlobalSettings.UmbracoMigrationName)] + [Migration("7.4.0", 4, Constants.System.UmbracoMigrationName)] public class RemoveParentIdPropertyTypeGroupColumn : MigrationBase { public RemoveParentIdPropertyTypeGroupColumn(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenOneZero/AssignMissingPrimaryForMySqlKeys.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenOneZero/AssignMissingPrimaryForMySqlKeys.cs index b9fbed209b..d4338f3a06 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenOneZero/AssignMissingPrimaryForMySqlKeys.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenOneZero/AssignMissingPrimaryForMySqlKeys.cs @@ -10,7 +10,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenOneZero // this is because when the 7.0.0 migrations are executed, this primary key get's created so if this migration is also executed // we will get exceptions because it is trying to create the PK two times. - [Migration("7.0.0", "7.1.0", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("7.0.0", "7.1.0", 0, Constants.System.UmbracoMigrationName)] public class AssignMissingPrimaryForMySqlKeys : MigrationBase { public AssignMissingPrimaryForMySqlKeys(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToCmsMemberLoginName.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToCmsMemberLoginName.cs new file mode 100644 index 0000000000..979faf27f2 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToCmsMemberLoginName.cs @@ -0,0 +1,34 @@ +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + [Migration("7.6.0", 0, Constants.System.UmbracoMigrationName)] + public class AddIndexToCmsMemberLoginName : MigrationBase + { + public AddIndexToCmsMemberLoginName(IMigrationContext context) + : base(context) + { } + + public override void Up() + { + var dbIndexes = SqlSyntax.GetDefinedIndexesDefinitions(Context.Database); + + //make sure it doesn't already exist + if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_cmsMember_LoginName")) == false) + { + Create.Index("IX_cmsMember_LoginName").OnTable("cmsMember") + .OnColumn("LoginName") + .Ascending() + .WithOptions() + .NonClustered(); + } + } + + public override void Down() + { + Delete.Index("IX_cmsMember_LoginName").OnTable("cmsMember"); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToUmbracoNodePath.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToUmbracoNodePath.cs new file mode 100644 index 0000000000..ef342a27ab --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToUmbracoNodePath.cs @@ -0,0 +1,34 @@ +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + [Migration("7.6.0", 0, Constants.System.UmbracoMigrationName)] + public class AddIndexToUmbracoNodePath : MigrationBase + { + public AddIndexToUmbracoNodePath(IMigrationContext context) + : base(context) + { } + + public override void Up() + { + var dbIndexes = SqlSyntax.GetDefinedIndexesDefinitions(Context.Database); + + //make sure it doesn't already exist + if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoNodePath")) == false) + { + Create.Index("IX_umbracoNodePath").OnTable("umbracoNode") + .OnColumn("path") + .Ascending() + .WithOptions() + .NonClustered(); + } + } + + public override void Down() + { + Delete.Index("IX_umbracoNodePath").OnTable("umbracoNode"); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToUser2NodePermission.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToUser2NodePermission.cs new file mode 100644 index 0000000000..5a3110900d --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToUser2NodePermission.cs @@ -0,0 +1,34 @@ +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + [Migration("7.6.0", 0, Constants.System.UmbracoMigrationName)] + public class AddIndexToUser2NodePermission : MigrationBase + { + public AddIndexToUser2NodePermission(IMigrationContext context) + : base(context) + { } + + public override void Up() + { + var dbIndexes = SqlSyntax.GetDefinedIndexesDefinitions(Context.Database); + + //make sure it doesn't already exist + if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoUser2NodePermission_nodeId")) == false) + { + Create.Index("IX_umbracoUser2NodePermission_nodeId").OnTable("umbracoUser2NodePermission") + .OnColumn("nodeId") + .Ascending() + .WithOptions() + .NonClustered(); + } + } + + public override void Down() + { + Delete.Index("IX_umbracoUser2NodePermission_nodeId").OnTable("cmsMember"); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexesToUmbracoRelationTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexesToUmbracoRelationTables.cs new file mode 100644 index 0000000000..1fa2f9f68a --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexesToUmbracoRelationTables.cs @@ -0,0 +1,91 @@ +using System; +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + [Migration("7.6.0", 0, Constants.System.UmbracoMigrationName)] + public class AddIndexesToUmbracoRelationTables : MigrationBase + { + public AddIndexesToUmbracoRelationTables(IMigrationContext context) + : base(context) + { } + + public override void Up() + { + var dbIndexes = SqlSyntax.GetDefinedIndexesDefinitions(Context.Database).ToArray(); + + //make sure it doesn't already exist + if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoRelation_parentChildType")) == false) + { + //This will remove any corrupt/duplicate data in the relation table before the index is applied + //Ensure this executes in a defered block which will be done inside of the migration transaction + this.Execute.Code(database => + { + //We need to check if this index has corrupted data and then clear that data + var duplicates = database.Fetch("SELECT parentId,childId,relType FROM umbracoRelation GROUP BY parentId,childId,relType HAVING COUNT(*) > 1"); + if (duplicates.Count > 0) + { + //need to fix this there cannot be duplicates so we'll take the latest entries, it's really not going to matter though + foreach (var duplicate in duplicates) + { + var ids = database.Fetch("SELECT id FROM umbracoRelation WHERE parentId=@parentId AND childId=@childId AND relType=@relType ORDER BY datetime DESC", + new { parentId = duplicate.parentId, childId = duplicate.childId, relType = duplicate.relType }); + + if (ids.Count == 1) + { + //this is just a safety check, this should absolutely never happen + throw new InvalidOperationException("Duplicates were detected but could not be discovered"); + } + + //delete the others + ids = ids.Skip(0).ToList(); + + //iterate in groups of 2000 to avoid the max sql parameter limit + foreach (var idGroup in ids.InGroupsOf(2000)) + { + database.Execute("DELETE FROM umbracoRelation WHERE id IN (@ids)", new { ids = idGroup }); + } + } + } + return ""; + }); + + //unique index to prevent duplicates - and for better perf + Create.Index("IX_umbracoRelation_parentChildType").OnTable("umbracoRelation") + .OnColumn("parentId").Ascending() + .OnColumn("childId").Ascending() + .OnColumn("relType").Ascending() + .WithOptions() + .Unique(); + } + + //need indexes on alias and name for relation type since these are queried against + + //make sure it doesn't already exist + if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoRelationType_alias")) == false) + { + Create.Index("IX_umbracoRelationType_alias").OnTable("umbracoRelationType") + .OnColumn("alias") + .Ascending() + .WithOptions() + .NonClustered(); + } + if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoRelationType_name")) == false) + { + Create.Index("IX_umbracoRelationType_name").OnTable("umbracoRelationType") + .OnColumn("name") + .Ascending() + .WithOptions() + .NonClustered(); + } + + } + + public override void Down() + { + Delete.Index("IX_umbracoNodePath").OnTable("umbracoNode"); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddLockObjects.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddLockObjects.cs new file mode 100644 index 0000000000..2dfe0d5dea --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddLockObjects.cs @@ -0,0 +1,38 @@ +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + [Migration("7.6.0", 101, Constants.System.UmbracoMigrationName)] + public class AddLockObjects : MigrationBase + { + public AddLockObjects(IMigrationContext context) + : base(context) + { } + + public override void Up() + { + EnsureLockObject(Constants.Locks.Servers, "Servers"); + } + + public override void Down() + { + // not implemented + } + + private void EnsureLockObject(int id, string name) + { + Execute.Code(db => + { + var exists = db.Exists(id); + if (exists) return string.Empty; + // be safe: delete old umbracoNode lock objects if any + db.Execute("DELETE FROM umbracoNode WHERE id=@id;", new { id }); + // then create umbracoLock object + db.Execute("INSERT umbracoLock (id, name, value) VALUES (@id, @name, 1);", new { id, name }); + return string.Empty; + }); + } + } +} diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddLockTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddLockTable.cs new file mode 100644 index 0000000000..a709378183 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddLockTable.cs @@ -0,0 +1,31 @@ +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + [Migration("7.6.0", 100, Constants.System.UmbracoMigrationName)] + public class AddLockTable : MigrationBase + { + public AddLockTable(IMigrationContext context) + : base(context) + { } + + public override void Up() + { + var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); + if (tables.InvariantContains("umbracoLock") == false) + { + Create.Table("umbracoLock") + .WithColumn("id").AsInt32().PrimaryKey("PK_umbracoLock") + .WithColumn("value").AsInt32().NotNullable() + .WithColumn("name").AsString(64).NotNullable(); + } + } + + public override void Down() + { + // not implemented + } + } +} diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddMacroUniqueIdColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddMacroUniqueIdColumn.cs new file mode 100644 index 0000000000..6e34d002de --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddMacroUniqueIdColumn.cs @@ -0,0 +1,74 @@ +using System; +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + [Migration("7.6.0", 0, Constants.System.UmbracoMigrationName)] + public class AddMacroUniqueIdColumn : MigrationBase + { + public AddMacroUniqueIdColumn(IMigrationContext context) + : base(context) + { } + + public override void Up() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); + + if (columns.Any(x => x.TableName.InvariantEquals("cmsMacro") && x.ColumnName.InvariantEquals("uniqueId")) == false) + { + Create.Column("uniqueId").OnTable("cmsMacro").AsGuid().Nullable(); + Execute.Code(UpdateMacroGuids); + Alter.Table("cmsMacro").AlterColumn("uniqueId").AsGuid().NotNullable(); + Create.Index("IX_cmsMacro_UniqueId").OnTable("cmsMacro").OnColumn("uniqueId") + .Ascending() + .WithOptions().NonClustered() + .WithOptions().Unique(); + + } + + if (columns.Any(x => x.TableName.InvariantEquals("cmsMacroProperty") && x.ColumnName.InvariantEquals("uniquePropertyId")) == false) + { + Create.Column("uniquePropertyId").OnTable("cmsMacroProperty").AsGuid().Nullable(); + Execute.Code(UpdateMacroPropertyGuids); + Alter.Table("cmsMacroProperty").AlterColumn("uniquePropertyId").AsGuid().NotNullable(); + Create.Index("IX_cmsMacroProperty_UniquePropertyId").OnTable("cmsMacroProperty").OnColumn("uniquePropertyId") + .Ascending() + .WithOptions().NonClustered() + .WithOptions().Unique(); + } + } + + private static string UpdateMacroGuids(IUmbracoDatabase database) + { + var updates = database.Query("SELECT id, macroAlias FROM cmsMacro") + .Select(macro => Tuple.Create((int) macro.id, ("macro____" + (string) macro.macroAlias).ToGuid())) + .ToList(); + + foreach (var update in updates) + database.Execute("UPDATE cmsMacro set uniqueId=@guid WHERE id=@id", new { guid = update.Item2, id = update.Item1 }); + + return string.Empty; + } + + private static string UpdateMacroPropertyGuids(IUmbracoDatabase database) + { + var updates = database.Query(@"SELECT cmsMacroProperty.id id, macroPropertyAlias propertyAlias, cmsMacro.macroAlias macroAlias +FROM cmsMacroProperty +JOIN cmsMacro ON cmsMacroProperty.macro=cmsMacro.id") + .Select(prop => Tuple.Create((int) prop.id, ("macro____" + (string) prop.macroAlias + "____" + (string) prop.propertyAlias).ToGuid())) + .ToList(); + + foreach (var update in updates) + database.Execute("UPDATE cmsMacroProperty set uniquePropertyId=@guid WHERE id=@id", new { guid = update.Item2, id = update.Item1 }); + + return string.Empty; + } + + public override void Down() + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddRelationTypeUniqueIdColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddRelationTypeUniqueIdColumn.cs new file mode 100644 index 0000000000..9525b4eba8 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddRelationTypeUniqueIdColumn.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + [Migration("7.6.0", 0, Constants.System.UmbracoMigrationName)] + public class AddRelationTypeUniqueIdColumn : MigrationBase + { + public AddRelationTypeUniqueIdColumn(IMigrationContext context) + : base(context) + { } + + public override void Up() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); + + if (columns.Any(x => x.TableName.InvariantEquals("umbracoRelationType") && x.ColumnName.InvariantEquals("typeUniqueId")) == false) + { + Create.Column("typeUniqueId").OnTable("umbracoRelationType").AsGuid().Nullable(); + Execute.Code(UpdateRelationTypeGuids); + Alter.Table("umbracoRelationType").AlterColumn("typeUniqueId").AsGuid().NotNullable(); + Create.Index("IX_umbracoRelationType_UniqueId").OnTable("umbracoRelationType").OnColumn("typeUniqueId") + .Ascending() + .WithOptions().NonClustered() + .WithOptions().Unique(); + } + } + + private static string UpdateRelationTypeGuids(IUmbracoDatabase database) + { + var updates = database.Query("SELECT id, alias, name FROM umbracoRelationType") + .Select(relationType => Tuple.Create((int) relationType.id, ("relationType____" + (string) relationType.alias + "____" + (string) relationType.name).ToGuid())) + .ToList(); + + foreach (var update in updates) + database.Execute("UPDATE umbracoRelationType set typeUniqueId=@guid WHERE id=@id", new { guid = update.Item2, id = update.Item1 }); + + return string.Empty; + } + + public override void Down() + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/RemovePropertyDataIdIndex.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/RemovePropertyDataIdIndex.cs index dabba48334..b6a5e42292 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/RemovePropertyDataIdIndex.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/RemovePropertyDataIdIndex.cs @@ -8,13 +8,12 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero /// /// See: http://issues.umbraco.org/issue/U4-9188 /// - [Migration("7.6.0", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("7.6.0", 0, Constants.System.UmbracoMigrationName)] public class UpdateUniqueIndexOnCmsPropertyData : MigrationBase { public UpdateUniqueIndexOnCmsPropertyData(IMigrationContext context) : base(context) - { - } + { } public override void Up() { diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/RemoveUmbracoDeployTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/RemoveUmbracoDeployTables.cs new file mode 100644 index 0000000000..f0a6267de3 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/RemoveUmbracoDeployTables.cs @@ -0,0 +1,39 @@ +using System; +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + [Migration("7.6.0", 0, Constants.System.UmbracoMigrationName)] + public class RemoveUmbracoDeployTables : MigrationBase + { + public RemoveUmbracoDeployTables(IMigrationContext context) + : base(context) + { } + + public override void Up() + { + var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); + + // there are two versions of umbracoDeployDependency, + // 1. one created by 7.4 and never used, we need to remove it (has a sourceId column) + // 2. one created by Deploy itself, we need to keep it (has a sourceUdi column) + if (tables.InvariantContains("umbracoDeployDependency")) + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); + if (columns.Any(x => x.TableName.InvariantEquals("umbracoDeployDependency") && x.ColumnName.InvariantEquals("sourceId"))) + Delete.Table("umbracoDeployDependency"); + } + + // always remove umbracoDeployChecksum + if (tables.InvariantContains("umbracoDeployChecksum")) + Delete.Table("umbracoDeployChecksum"); + } + + public override void Down() + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeOne/UpdateUserLanguagesToIsoCode.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeOne/UpdateUserLanguagesToIsoCode.cs index c114501188..20e4ad59a3 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeOne/UpdateUserLanguagesToIsoCode.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeOne/UpdateUserLanguagesToIsoCode.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeOn /// /// This fixes the storage of user languages from the old format like en_us to en-US /// - [Migration("7.3.1", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.1", 0, Constants.System.UmbracoMigrationName)] public class UpdateUserLanguagesToIsoCode : MigrationBase { public UpdateUserLanguagesToIsoCode(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeTwo/EnsureMigrationsTableIdentityIsCorrect.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeTwo/EnsureMigrationsTableIdentityIsCorrect.cs index a431bf4524..1b9d722e4a 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeTwo/EnsureMigrationsTableIdentityIsCorrect.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeTwo/EnsureMigrationsTableIdentityIsCorrect.cs @@ -10,7 +10,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeTw /// This reinserts all migrations in the migrations table to account for initial rows inserted /// on creation without identity enabled. /// - [Migration("7.3.2", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.2", 0, Constants.System.UmbracoMigrationName)] public class EnsureMigrationsTableIdentityIsCorrect : MigrationBase { public EnsureMigrationsTableIdentityIsCorrect(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddExternalLoginsTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddExternalLoginsTable.cs index 2249b5931f..e5beef1cfa 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddExternalLoginsTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddExternalLoginsTable.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 9, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 9, Constants.System.UmbracoMigrationName)] public class AddExternalLoginsTable : MigrationBase { public AddExternalLoginsTable(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddForeignKeysForLanguageAndDictionaryTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddForeignKeysForLanguageAndDictionaryTables.cs index 1703c56fcd..b48839240f 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddForeignKeysForLanguageAndDictionaryTables.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddForeignKeysForLanguageAndDictionaryTables.cs @@ -9,7 +9,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 14, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 14, Constants.System.UmbracoMigrationName)] public class AddForeignKeysForLanguageAndDictionaryTables : MigrationBase { public AddForeignKeysForLanguageAndDictionaryTables(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddMigrationTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddMigrationTable.cs index 14d7964e49..9b71c8b0d6 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddMigrationTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddMigrationTable.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 11, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 11, Constants.System.UmbracoMigrationName)] public class AddMigrationTable : MigrationBase { public AddMigrationTable(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddPublicAccessTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddPublicAccessTables.cs index fe353bf90e..ea85c053aa 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddPublicAccessTables.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddPublicAccessTables.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 6, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 6, Constants.System.UmbracoMigrationName)] public class AddPublicAccessTables : MigrationBase { public AddPublicAccessTables(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddRelationTypeForDocumentOnDelete.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddRelationTypeForDocumentOnDelete.cs index 672db2e638..d93ac1a55c 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddRelationTypeForDocumentOnDelete.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddRelationTypeForDocumentOnDelete.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 0, Constants.System.UmbracoMigrationName)] public class AddRelationTypeForDocumentOnDelete : MigrationBase { public AddRelationTypeForDocumentOnDelete(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddServerRegistrationColumnsAndLock.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddServerRegistrationColumnsAndLock.cs index 5e6a5e1c8c..ca58e41adf 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddServerRegistrationColumnsAndLock.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddServerRegistrationColumnsAndLock.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 17, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 17, Constants.System.UmbracoMigrationName)] public class AddServerRegistrationColumnsAndLock : MigrationBase { public AddServerRegistrationColumnsAndLock(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUniqueIdPropertyTypeColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUniqueIdPropertyTypeColumn.cs index 0e04b6ee9b..49966d89b2 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUniqueIdPropertyTypeColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUniqueIdPropertyTypeColumn.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 13, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 13, Constants.System.UmbracoMigrationName)] public class AddUniqueIdPropertyTypeColumn : MigrationBase { public AddUniqueIdPropertyTypeColumn(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUserColumns.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUserColumns.cs index 797808eee7..a91ceb25ca 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUserColumns.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUserColumns.cs @@ -4,7 +4,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 10, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 10, Constants.System.UmbracoMigrationName)] public class AddUserColumns : MigrationBase { public AddUserColumns(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/CleanUpCorruptedPublishedFlags.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/CleanUpCorruptedPublishedFlags.cs index 18e2ea91f7..102bdcc896 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/CleanUpCorruptedPublishedFlags.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/CleanUpCorruptedPublishedFlags.cs @@ -10,7 +10,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZe /// Older corrupted dbs might have multiple published flags for a content item, this shouldn't be possible /// so we need to clear the content flag on the older version /// - [Migration("7.3.0", 18, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 18, Constants.System.UmbracoMigrationName)] public class CleanUpCorruptedPublishedFlags : MigrationBase { public CleanUpCorruptedPublishedFlags(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/CreateCacheInstructionTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/CreateCacheInstructionTable.cs index d35136691d..6b21536e1b 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/CreateCacheInstructionTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/CreateCacheInstructionTable.cs @@ -9,7 +9,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 1, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 1, Constants.System.UmbracoMigrationName)] public class CreateCacheInstructionTable : MigrationBase { public CreateCacheInstructionTable(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MigrateAndRemoveTemplateMasterColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MigrateAndRemoveTemplateMasterColumn.cs index c4ea9016c7..56c02f16c7 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MigrateAndRemoveTemplateMasterColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MigrateAndRemoveTemplateMasterColumn.cs @@ -13,7 +13,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZe /// /// Remove the master column after we've migrated all of the values into the 'ParentId' and Path column of Umbraco node /// - [Migration("7.3.0", 1, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 1, Constants.System.UmbracoMigrationName)] public class MigrateAndRemoveTemplateMasterColumn : MigrationBase { diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MigrateStylesheetDataToFile.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MigrateStylesheetDataToFile.cs index d0195555b5..0bb9e83227 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MigrateStylesheetDataToFile.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MigrateStylesheetDataToFile.cs @@ -19,7 +19,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZe /// These files will then be copied over once the entire migration is complete so that if any migration fails and the db changes are /// rolled back, the original files won't be affected. /// - [Migration("7.3.0", 2, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 2, Constants.System.UmbracoMigrationName)] public class MigrateStylesheetDataToFile : MigrationBase { public MigrateStylesheetDataToFile(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MovePublicAccessXmlDataToDb.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MovePublicAccessXmlDataToDb.cs index fde161b014..d891902537 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MovePublicAccessXmlDataToDb.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MovePublicAccessXmlDataToDb.cs @@ -11,7 +11,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 7, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 7, Constants.System.UmbracoMigrationName)] public class MovePublicAccessXmlDataToDb : MigrationBase { public MovePublicAccessXmlDataToDb(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveHelpTextColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveHelpTextColumn.cs index 767622b076..8e0468503a 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveHelpTextColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveHelpTextColumn.cs @@ -4,7 +4,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 8, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 8, Constants.System.UmbracoMigrationName)] public class RemoveHelpTextColumn : MigrationBase { public RemoveHelpTextColumn(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveLanguageLocaleColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveLanguageLocaleColumn.cs index d842c14324..d72fdd9094 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveLanguageLocaleColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveLanguageLocaleColumn.cs @@ -4,7 +4,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 4, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 4, Constants.System.UmbracoMigrationName)] public class RemoveLanguageLocaleColumn : MigrationBase { public RemoveLanguageLocaleColumn(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveStylesheetDataAndTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveStylesheetDataAndTables.cs index 517de15665..3421f0d22b 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveStylesheetDataAndTables.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveStylesheetDataAndTables.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 3, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 3, Constants.System.UmbracoMigrationName)] public class RemoveStylesheetDataAndTables : MigrationBase { public RemoveStylesheetDataAndTables(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveUmbracoLoginsTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveUmbracoLoginsTable.cs index 8142f81169..bb673f0406 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveUmbracoLoginsTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveUmbracoLoginsTable.cs @@ -4,7 +4,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 15, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 15, Constants.System.UmbracoMigrationName)] public class RemoveUmbracoLoginsTable : MigrationBase { public RemoveUmbracoLoginsTable(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/UpdateUniqueIdToHaveCorrectIndexType.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/UpdateUniqueIdToHaveCorrectIndexType.cs index 3729bd84a2..162c4f618e 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/UpdateUniqueIdToHaveCorrectIndexType.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/UpdateUniqueIdToHaveCorrectIndexType.cs @@ -7,7 +7,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZe { //see: http://issues.umbraco.org/issue/U4-6188, http://issues.umbraco.org/issue/U4-6187 - [Migration("7.3.0", 5, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 5, Constants.System.UmbracoMigrationName)] public class UpdateUniqueIdToHaveCorrectIndexType : MigrationBase { public UpdateUniqueIdToHaveCorrectIndexType(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/AddIndexToUmbracoNodeTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/AddIndexToUmbracoNodeTable.cs index d794a2e606..edcf481518 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/AddIndexToUmbracoNodeTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/AddIndexToUmbracoNodeTable.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenTwoZero { - [Migration("7.2.0", 3, GlobalSettings.UmbracoMigrationName)] + [Migration("7.2.0", 3, Constants.System.UmbracoMigrationName)] public class AddIndexToUmbracoNodeTable : MigrationBase { private readonly bool _skipIndexCheck; diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/AddMissingForeignKeyForContentType.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/AddMissingForeignKeyForContentType.cs index 9efd6d86e2..2e5c90fb78 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/AddMissingForeignKeyForContentType.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/AddMissingForeignKeyForContentType.cs @@ -8,7 +8,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenTwoZero { - [Migration("7.2.0", 1, GlobalSettings.UmbracoMigrationName)] + [Migration("7.2.0", 1, Constants.System.UmbracoMigrationName)] public class AddMissingForeignKeyForContentType : MigrationBase { public AddMissingForeignKeyForContentType(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/AlterDataTypePreValueTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/AlterDataTypePreValueTable.cs index 1f3c0d64a1..982cee048f 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/AlterDataTypePreValueTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/AlterDataTypePreValueTable.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenTwoZero { - [Migration("7.2.0", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("7.2.0", 0, Constants.System.UmbracoMigrationName)] public class AlterDataTypePreValueTable : MigrationBase { public AlterDataTypePreValueTable(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/RemoveCmsDocumentAliasColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/RemoveCmsDocumentAliasColumn.cs index 3dd971c9c2..0c907b534c 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/RemoveCmsDocumentAliasColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/RemoveCmsDocumentAliasColumn.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenTwoZero { - [Migration("7.2.0", 2, GlobalSettings.UmbracoMigrationName)] + [Migration("7.2.0", 2, Constants.System.UmbracoMigrationName)] public class RemoveCmsDocumentAliasColumn : MigrationBase { public RemoveCmsDocumentAliasColumn(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/DeleteAppTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/DeleteAppTables.cs index b0dd2416e2..fc3039705f 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/DeleteAppTables.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/DeleteAppTables.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSix { - [Migration("6.0.0", 10, GlobalSettings.UmbracoMigrationName)] + [Migration("6.0.0", 10, Constants.System.UmbracoMigrationName)] public class DeleteAppTables : MigrationBase { public DeleteAppTables(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/EnsureAppsTreesUpdated.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/EnsureAppsTreesUpdated.cs index ba7afe9c75..4bf55908bd 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/EnsureAppsTreesUpdated.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/EnsureAppsTreesUpdated.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSix { - [Migration("6.0.0", 9, GlobalSettings.UmbracoMigrationName)] + [Migration("6.0.0", 9, Constants.System.UmbracoMigrationName)] public class EnsureAppsTreesUpdated : MigrationBase { public EnsureAppsTreesUpdated(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/MoveMasterContentTypeData.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/MoveMasterContentTypeData.cs index 16c301df0a..7ec1689ccd 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/MoveMasterContentTypeData.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/MoveMasterContentTypeData.cs @@ -4,7 +4,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSix { - [Migration("6.0.0", 5, GlobalSettings.UmbracoMigrationName)] + [Migration("6.0.0", 5, Constants.System.UmbracoMigrationName)] public class MoveMasterContentTypeData : MigrationBase { public MoveMasterContentTypeData(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/NewCmsContentType2ContentTypeTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/NewCmsContentType2ContentTypeTable.cs index 1ccc600a6c..3d907ab754 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/NewCmsContentType2ContentTypeTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/NewCmsContentType2ContentTypeTable.cs @@ -4,7 +4,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSix { - [Migration("6.0.0", 4, GlobalSettings.UmbracoMigrationName)] + [Migration("6.0.0", 4, Constants.System.UmbracoMigrationName)] public class NewCmsContentType2ContentTypeTable : MigrationBase { public NewCmsContentType2ContentTypeTable(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/RemoveMasterContentTypeColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/RemoveMasterContentTypeColumn.cs index 2634b64160..af0cbe14cc 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/RemoveMasterContentTypeColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/RemoveMasterContentTypeColumn.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSix { - [Migration("6.0.0", 6, GlobalSettings.UmbracoMigrationName)] + [Migration("6.0.0", 6, Constants.System.UmbracoMigrationName)] public class RemoveMasterContentTypeColumn : MigrationBase { public RemoveMasterContentTypeColumn(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/RenameCmsTabTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/RenameCmsTabTable.cs index ceab9faa18..21ad3841cd 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/RenameCmsTabTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/RenameCmsTabTable.cs @@ -4,7 +4,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSix { - [Migration("6.0.0", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("6.0.0", 0, Constants.System.UmbracoMigrationName)] public class RenameCmsTabTable : MigrationBase { public RenameCmsTabTable(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/RenameTabIdColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/RenameTabIdColumn.cs index 872a167682..9ea074abc6 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/RenameTabIdColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/RenameTabIdColumn.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSix { - [Migration("6.0.0", 7, GlobalSettings.UmbracoMigrationName)] + [Migration("6.0.0", 7, Constants.System.UmbracoMigrationName)] public class RenameTabIdColumn : MigrationBase { public RenameTabIdColumn(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsContentTypeAllowedContentTypeTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsContentTypeAllowedContentTypeTable.cs index b4b48432d0..e8ac2c0387 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsContentTypeAllowedContentTypeTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsContentTypeAllowedContentTypeTable.cs @@ -4,7 +4,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSix { - [Migration("6.0.0", 3, GlobalSettings.UmbracoMigrationName)] + [Migration("6.0.0", 3, Constants.System.UmbracoMigrationName)] public class UpdateCmsContentTypeAllowedContentTypeTable : MigrationBase { public UpdateCmsContentTypeAllowedContentTypeTable(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsContentTypeTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsContentTypeTable.cs index 5e66533af2..8948bab1ff 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsContentTypeTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsContentTypeTable.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSix { - [Migration("6.0.0", 2, GlobalSettings.UmbracoMigrationName)] + [Migration("6.0.0", 2, Constants.System.UmbracoMigrationName)] public class UpdateCmsContentTypeTable : MigrationBase { public UpdateCmsContentTypeTable(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsContentVersionTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsContentVersionTable.cs index 566c4f7891..2c41d408b0 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsContentVersionTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsContentVersionTable.cs @@ -4,7 +4,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSix { - [Migration("6.0.0", 8, GlobalSettings.UmbracoMigrationName)] + [Migration("6.0.0", 8, Constants.System.UmbracoMigrationName)] public class UpdateCmsContentVersionTable : MigrationBase { public UpdateCmsContentVersionTable(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsPropertyTypeGroupTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsPropertyTypeGroupTable.cs index ff7e9f43f0..120b60755e 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsPropertyTypeGroupTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsPropertyTypeGroupTable.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSix { - [Migration("6.0.0", 1, GlobalSettings.UmbracoMigrationName)] + [Migration("6.0.0", 1, Constants.System.UmbracoMigrationName)] public class UpdateCmsPropertyTypeGroupTable : MigrationBase { public UpdateCmsPropertyTypeGroupTable(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixOneZero/CreateServerRegistryTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixOneZero/CreateServerRegistryTable.cs index 6d517fcc98..346a09bd2f 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixOneZero/CreateServerRegistryTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixOneZero/CreateServerRegistryTable.cs @@ -10,7 +10,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSixOneZero { - [Migration("6.1.0", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("6.1.0", 0, Constants.System.UmbracoMigrationName)] public class CreateServerRegistryTable : MigrationBase { public CreateServerRegistryTable(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AddChangeDocumentTypePermission.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AddChangeDocumentTypePermission.cs index b87626eb8e..eb1669b252 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AddChangeDocumentTypePermission.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AddChangeDocumentTypePermission.cs @@ -7,8 +7,8 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSixTwoZero { - [Migration("7.1.0", 3, GlobalSettings.UmbracoMigrationName)] - [Migration("6.2.0", 3, GlobalSettings.UmbracoMigrationName)] + [Migration("7.1.0", 3, Constants.System.UmbracoMigrationName)] + [Migration("6.2.0", 3, Constants.System.UmbracoMigrationName)] public class AddChangeDocumentTypePermission : MigrationBase { public AddChangeDocumentTypePermission(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AdditionalIndexesAndKeys.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AdditionalIndexesAndKeys.cs index 9781bada36..3e9394c2a8 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AdditionalIndexesAndKeys.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AdditionalIndexesAndKeys.cs @@ -8,8 +8,8 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSixTwoZero { - [Migration("7.1.0", 1, GlobalSettings.UmbracoMigrationName)] - [Migration("6.2.0", 1, GlobalSettings.UmbracoMigrationName)] + [Migration("7.1.0", 1, Constants.System.UmbracoMigrationName)] + [Migration("6.2.0", 1, Constants.System.UmbracoMigrationName)] public class AdditionalIndexesAndKeys : MigrationBase { public AdditionalIndexesAndKeys(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AssignMissingPrimaryForMySqlKeys.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AssignMissingPrimaryForMySqlKeys.cs index 9d34281f50..9c86056aa7 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AssignMissingPrimaryForMySqlKeys.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AssignMissingPrimaryForMySqlKeys.cs @@ -8,8 +8,8 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSixTwoZero { //see: http://issues.umbraco.org/issue/U4-4430 - [Migration("7.1.0", 0, GlobalSettings.UmbracoMigrationName)] - [Migration("6.2.0", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("7.1.0", 0, Constants.System.UmbracoMigrationName)] + [Migration("6.2.0", 0, Constants.System.UmbracoMigrationName)] public class AssignMissingPrimaryForMySqlKeys : MigrationBase { public AssignMissingPrimaryForMySqlKeys(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AssignMissingPrimaryForMySqlKeys2.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AssignMissingPrimaryForMySqlKeys2.cs index b1beca51d6..81be21bf92 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AssignMissingPrimaryForMySqlKeys2.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AssignMissingPrimaryForMySqlKeys2.cs @@ -8,7 +8,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSixTwoZero //We have to target this specifically to ensure this DOES NOT execute if upgrading from a version previous to 6.0, // this is because when the 6.0.0 migrations are executed, this primary key get's created so if this migration is also executed // we will get exceptions because it is trying to create the PK two times. - [Migration("6.0.0", "6.2.0", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("6.0.0", "6.2.0", 0, Constants.System.UmbracoMigrationName)] public class AssignMissingPrimaryForMySqlKeys2 : MigrationBase { public AssignMissingPrimaryForMySqlKeys2(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/ChangePasswordColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/ChangePasswordColumn.cs index 83ec557e8b..298b0a44f0 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/ChangePasswordColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/ChangePasswordColumn.cs @@ -5,8 +5,8 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSixTwoZero { - [Migration("7.1.0", 2, GlobalSettings.UmbracoMigrationName)] - [Migration("6.2.0", 2, GlobalSettings.UmbracoMigrationName)] + [Migration("7.1.0", 2, Constants.System.UmbracoMigrationName)] + [Migration("6.2.0", 2, Constants.System.UmbracoMigrationName)] public class ChangePasswordColumn : MigrationBase { public ChangePasswordColumn(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/UpdateToNewMemberPropertyAliases.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/UpdateToNewMemberPropertyAliases.cs index 788a382c42..2d639fe6d7 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/UpdateToNewMemberPropertyAliases.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/UpdateToNewMemberPropertyAliases.cs @@ -8,8 +8,8 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSixTwoZero { - [Migration("7.1.0", 4, GlobalSettings.UmbracoMigrationName)] - [Migration("6.2.0", 4, GlobalSettings.UmbracoMigrationName)] + [Migration("7.1.0", 4, Constants.System.UmbracoMigrationName)] + [Migration("6.2.0", 4, Constants.System.UmbracoMigrationName)] public class UpdateToNewMemberPropertyAliases : MigrationBase { public UpdateToNewMemberPropertyAliases(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixZeroOne/UpdatePropertyTypesAndGroups.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixZeroOne/UpdatePropertyTypesAndGroups.cs index 9ffc0bb93c..1e51558508 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixZeroOne/UpdatePropertyTypesAndGroups.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixZeroOne/UpdatePropertyTypesAndGroups.cs @@ -8,7 +8,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSixZeroOne { - [Migration("6.0.2", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("6.0.2", 0, Constants.System.UmbracoMigrationName)] public class UpdatePropertyTypesAndGroups : MigrationBase { public UpdatePropertyTypesAndGroups(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs b/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs new file mode 100644 index 0000000000..b9b9e6c28a --- /dev/null +++ b/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs @@ -0,0 +1,249 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Data.SqlServerCe; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NPoco; +using StackExchange.Profiling.Data; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence +{ + /// + /// Provides extension methods to NPoco Database class. + /// + public static partial class NPocoDatabaseExtensions + { + // todo: review NPoco native InsertBulk to replace the code below + + /// + /// Bulk-inserts records within a transaction. + /// + /// The type of the records. + /// The database. + /// The records. + /// Whether to use native bulk insert when available. + public static void BulkInsertRecordsWithTransaction(this IUmbracoDatabase database, IEnumerable records, bool useNativeBulkInsert = true) + { + var recordsA = records.ToArray(); + if (recordsA.Length == 0) + return; + + // no need to "try...catch", if the transaction is not completed it will rollback! + using (var tr = database.GetTransaction()) + { + database.BulkInsertRecords(recordsA, useNativeBulkInsert); + tr.Complete(); + } + } + + /// + /// Bulk-inserts records. + /// + /// The type of the records. + /// The database. + /// The records. + /// Whether to use native bulk insert when available. + /// The number of records that were inserted. + public static int BulkInsertRecords(this IUmbracoDatabase database, IEnumerable records, bool useNativeBulkInsert = true) + { + var recordsA = records.ToArray(); + if (recordsA.Length == 0) return 0; + + var pocoData = database.PocoDataFactory.ForType(typeof(T)); + if (pocoData == null) throw new InvalidOperationException("Could not find PocoData for " + typeof(T)); + + if (database.DatabaseType.IsSqlCe()) + { + if (useNativeBulkInsert) return BulkInsertRecordsSqlCe(database, pocoData, recordsA); + // else, no other choice + foreach (var record in recordsA) + database.Insert(record); + return recordsA.Length; + } + + if (database.DatabaseType.IsSqlServer()) + { + return useNativeBulkInsert && database.DatabaseType.IsSqlServer2008OrLater() + ? BulkInsertRecordsSqlServer(database, pocoData, recordsA) + : BulkInsertRecordsWithCommands(database, recordsA); + } + + if (database.DatabaseType.IsMySql()) + return BulkInsertRecordsWithCommands(database, recordsA); + + throw new NotSupportedException(); + } + + /// + /// Bulk-insert records using commands. + /// + /// The type of the records. + /// The database. + /// The records. + /// The number of records that were inserted. + private static int BulkInsertRecordsWithCommands(IUmbracoDatabase database, T[] records) + { + foreach (var command in database.GenerateBulkInsertCommands(records)) + command.ExecuteNonQuery(); + + return records.Length; // what else? + } + + /// + /// 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) throw new ArgumentException("Null database?.connection.", nameof(database)); + + var pocoData = database.PocoDataFactory.ForType(typeof(T)); + + // get columns to include, = number of parameters per row + var 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(2000.00 / 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++) + { + var 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--) + { + var 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) + ")"; + } + + command.CommandText = $"INSERT INTO {tableName} ({columnNames}) VALUES {string.Join(", ", recordsValues)}"; + commands[commandIndex] = command; + } + + return commands; + } + + /// + /// 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. + private static bool IncludeColumn(PocoData pocoData, KeyValuePair column) + { + return column.Value.ResultColumn == false + && (pocoData.TableInfo.AutoIncrement == false || column.Key != pocoData.TableInfo.PrimaryKey); + } + + /// + /// Bulk-insert records using SqlCE TableDirect method. + /// + /// The type of the records. + /// The database. + /// The PocoData object corresponding to the record's type. + /// The records. + /// The number of records that were inserted. + internal static int BulkInsertRecordsSqlCe(IUmbracoDatabase database, PocoData pocoData, IEnumerable records) + { + var columns = pocoData.Columns.ToArray(); + + // create command against the original database.Connection + using (var command = database.CreateCommand(database.Connection, CommandType.TableDirect, string.Empty)) + { + command.CommandText = pocoData.TableInfo.TableName; + command.CommandType = CommandType.TableDirect; // fixme - why repeat? + // fixme - not supporting transactions? + //cmd.Transaction = GetTypedTransaction(db.Connection.); + + var count = 0; + var tCommand = GetTypedCommand(command); // execute on the real command + + // seems to cause problems, I think this is primarily used for retrieval, not inserting. + // see: https://msdn.microsoft.com/en-us/library/system.data.sqlserverce.sqlcecommand.indexname%28v=vs.100%29.aspx?f=255&MSPPError=-2147217396 + //tCommand.IndexName = pd.TableInfo.PrimaryKey; + + using (var resultSet = tCommand.ExecuteResultSet(ResultSetOptions.Updatable)) + { + var updatableRecord = resultSet.CreateRecord(); + foreach (var record in records) + { + for (var i = 0; i < columns.Length; i++) + { + // skip the index if this shouldn't be included (i.e. PK) + if (IncludeColumn(pocoData, columns[i])) + { + var val = columns[i].Value.GetValue(record); + updatableRecord.SetValue(i, val); + } + } + resultSet.Insert(updatableRecord); + count++; + } + } + + return count; + } + } + + /// + /// Bulk-insert records using SqlServer BulkCopy method. + /// + /// The type of the records. + /// The database. + /// The PocoData object corresponding to the record's type. + /// The records. + /// The number of records that were inserted. + internal static int BulkInsertRecordsSqlServer(IUmbracoDatabase database, PocoData pocoData, IEnumerable records) + { + // create command against the original database.Connection + using (var command = database.CreateCommand(database.Connection, CommandType.Text, string.Empty)) + { + // use typed connection and transactionf or SqlBulkCopy + var tConnection = GetTypedConnection(database.Connection); + var tTransaction = GetTypedTransaction(command.Transaction); + var tableName = pocoData.TableInfo.TableName; + + var syntax = database.SqlSyntax as SqlServerSyntaxProvider; + if (syntax == null) throw new NotSupportedException("SqlSyntax must be SqlServerSyntaxProvider."); + + using (var copy = new SqlBulkCopy(tConnection, SqlBulkCopyOptions.Default, tTransaction) { BulkCopyTimeout = 10000, DestinationTableName = tableName }) + using (var bulkReader = new PocoDataDataReader(records, pocoData, syntax)) + { + copy.WriteToServer(bulkReader); + return bulkReader.RecordsAffected; + } + } + } + + } +} diff --git a/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions.cs b/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions.cs index 9b7ede89f0..34ef2d536d 100644 --- a/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions.cs +++ b/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions.cs @@ -16,7 +16,7 @@ namespace Umbraco.Core.Persistence /// /// Provides extension methods to NPoco Database class. /// - public static class NPocoDatabaseExtensions + public static partial class NPocoDatabaseExtensions { // NOTE // @@ -105,6 +105,9 @@ namespace Umbraco.Core.Persistence if (poco == null) throw new ArgumentNullException(nameof(poco)); + // fixme - 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() ? db.Update(poco) @@ -159,232 +162,6 @@ namespace Umbraco.Core.Persistence return regex.Replace(value, "@@"); } - // todo: review NPoco native InsertBulk to replace the code below - - /// - /// Bulk-inserts records within a transaction. - /// - /// The type of the records. - /// The database. - /// The records. - /// Whether to use native bulk insert when available. - public static void BulkInsertRecordsWithTransaction(this IUmbracoDatabase database, IEnumerable records, bool useNativeBulkInsert = true) - { - var recordsA = records.ToArray(); - if (recordsA.Length == 0) - return; - - // no need to "try...catch", if the transaction is not completed it will rollback! - using (var tr = database.GetTransaction()) - { - database.BulkInsertRecords(recordsA, useNativeBulkInsert); - tr.Complete(); - } - } - - /// - /// Bulk-inserts records. - /// - /// The type of the records. - /// The database. - /// The records. - /// Whether to use native bulk insert when available. - /// The number of records that were inserted. - public static int BulkInsertRecords(this IUmbracoDatabase database, IEnumerable records, bool useNativeBulkInsert = true) - { - var recordsA = records.ToArray(); - if (recordsA.Length == 0) return 0; - - var pocoData = database.PocoDataFactory.ForType(typeof(T)); - if (pocoData == null) throw new InvalidOperationException("Could not find PocoData for " + typeof(T)); - - if (database.DatabaseType.IsSqlCe()) - { - if (useNativeBulkInsert) return BulkInsertRecordsSqlCe(database, pocoData, recordsA); - // else, no other choice - foreach (var record in recordsA) - database.Insert(record); - return recordsA.Length; - } - - if (database.DatabaseType.IsSqlServer()) - { - return useNativeBulkInsert && database.DatabaseType.IsSqlServer2008OrLater() - ? BulkInsertRecordsSqlServer(database, pocoData, recordsA) - : BulkInsertRecordsWithCommands(database, recordsA); - } - - if (database.DatabaseType.IsMySql()) - return BulkInsertRecordsWithCommands(database, recordsA); - - throw new NotSupportedException(); - } - - /// - /// Bulk-insert records using commands. - /// - /// The type of the records. - /// The database. - /// The records. - /// The number of records that were inserted. - private static int BulkInsertRecordsWithCommands(IUmbracoDatabase database, T[] records) - { - foreach (var command in database.GenerateBulkInsertCommands(records)) - command.ExecuteNonQuery(); - - return records.Length; // what else? - } - - /// - /// 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) - { - var pocoData = database.PocoDataFactory.ForType(typeof(T)); - - // get columns to include, = number of parameters per row - var 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(2000.00 / 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++) - { - var 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--) - { - var 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) + ")"; - } - - command.CommandText = $"INSERT INTO {tableName} ({columnNames}) VALUES {string.Join(", ", recordsValues)}"; - commands[commandIndex] = command; - } - - return commands; - } - - /// - /// 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. - private static bool IncludeColumn(PocoData pocoData, KeyValuePair column) - { - return column.Value.ResultColumn == false - && (pocoData.TableInfo.AutoIncrement == false || column.Key != pocoData.TableInfo.PrimaryKey); - } - - /// - /// Bulk-insert records using SqlCE TableDirect method. - /// - /// The type of the records. - /// The database. - /// The PocoData object corresponding to the record's type. - /// The records. - /// The number of records that were inserted. - internal static int BulkInsertRecordsSqlCe(IUmbracoDatabase database, PocoData pocoData, IEnumerable records) - { - var columns = pocoData.Columns.ToArray(); - - // create command against the original database.Connection - using (var command = database.CreateCommand(database.Connection, CommandType.TableDirect, string.Empty)) - { - command.CommandText = pocoData.TableInfo.TableName; - command.CommandType = CommandType.TableDirect; // fixme - why repeat? - // fixme - not supporting transactions? - //cmd.Transaction = GetTypedTransaction(db.Connection.); - - var count = 0; - var tCommand = GetTypedCommand(command); // execute on the real command - - // seems to cause problems, I think this is primarily used for retrieval, not inserting. - // see: https://msdn.microsoft.com/en-us/library/system.data.sqlserverce.sqlcecommand.indexname%28v=vs.100%29.aspx?f=255&MSPPError=-2147217396 - //tCommand.IndexName = pd.TableInfo.PrimaryKey; - - using (var resultSet = tCommand.ExecuteResultSet(ResultSetOptions.Updatable)) - { - var updatableRecord = resultSet.CreateRecord(); - foreach (var record in records) - { - for (var i = 0; i < columns.Length; i++) - { - // skip the index if this shouldn't be included (i.e. PK) - if (IncludeColumn(pocoData, columns[i])) - { - var val = columns[i].Value.GetValue(record); - updatableRecord.SetValue(i, val); - } - } - resultSet.Insert(updatableRecord); - count++; - } - } - - return count; - } - } - - /// - /// Bulk-insert records using SqlServer BulkCopy method. - /// - /// The type of the records. - /// The database. - /// The PocoData object corresponding to the record's type. - /// The records. - /// The number of records that were inserted. - internal static int BulkInsertRecordsSqlServer(IUmbracoDatabase database, PocoData pocoData, IEnumerable records) - { - // create command against the original database.Connection - using (var command = database.CreateCommand(database.Connection, CommandType.Text, string.Empty)) - { - // use typed connection and transactionf or SqlBulkCopy - var tConnection = GetTypedConnection(database.Connection); - var tTransaction = GetTypedTransaction(command.Transaction); - var tableName = pocoData.TableInfo.TableName; - - var syntax = database.SqlSyntax as SqlServerSyntaxProvider; - if (syntax == null) throw new NotSupportedException("SqlSyntax must be SqlServerSyntaxProvider."); - - using (var copy = new SqlBulkCopy(tConnection, SqlBulkCopyOptions.Default, tTransaction) { BulkCopyTimeout = 10000, DestinationTableName = tableName }) - using (var bulkReader = new PocoDataDataReader(records, pocoData, syntax)) - { - copy.WriteToServer(bulkReader); - return bulkReader.RecordsAffected; - } - } - } - /// /// Returns the underlying connection as a typed connection - this is used to unwrap the profiled mini profiler stuff /// diff --git a/src/Umbraco.Core/Persistence/Querying/CachedExpression.cs b/src/Umbraco.Core/Persistence/Querying/CachedExpression.cs new file mode 100644 index 0000000000..5c502ec170 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Querying/CachedExpression.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq.Expressions; + +namespace Umbraco.Core.Persistence.Querying +{ + /// + /// Represents an expression which caches the visitor's result. + /// + internal class CachedExpression : Expression + { + private string _visitResult; + + /// + /// Gets or sets the inner Expression. + /// + public Expression InnerExpression { get; private set; } + + /// + /// Gets or sets the compiled SQL statement output. + /// + public string VisitResult + { + get { return _visitResult; } + set + { + if (Visited) + throw new InvalidOperationException("Cached expression has already been visited."); + _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; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Querying/ExpressionHelper.cs b/src/Umbraco.Core/Persistence/Querying/ExpressionHelper.cs deleted file mode 100644 index 4a97dc571c..0000000000 --- a/src/Umbraco.Core/Persistence/Querying/ExpressionHelper.cs +++ /dev/null @@ -1,572 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Globalization; -using System.Linq; -using System.Linq.Expressions; -using System.Text; -using Umbraco.Core.Persistence.Mappers; - -namespace Umbraco.Core.Persistence.Querying -{ - internal class ExpressionHelper - { - private string selectExpression = string.Empty; - private string whereExpression; - private string groupBy = string.Empty; - private string havingExpression; - private string orderBy = string.Empty; - - IList updateFields = new List(); - IList insertFields = new List(); - - private string sep = string.Empty; - private bool useFieldName = false; - private Database.PocoData pd; - - public ExpressionHelper() - { - Database.Mapper = new ModelDtoMapper(); - pd = new Database.PocoData(typeof(T)); - } - - protected internal virtual string Visit(Expression exp) - { - - if (exp == null) return string.Empty; - switch (exp.NodeType) - { - case ExpressionType.Lambda: - return VisitLambda(exp as LambdaExpression); - case ExpressionType.MemberAccess: - return VisitMemberAccess(exp as MemberExpression); - case ExpressionType.Constant: - return VisitConstant(exp as ConstantExpression); - 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: - return "(" + VisitBinary(exp as BinaryExpression) + ")"; - case ExpressionType.Negate: - case ExpressionType.NegateChecked: - case ExpressionType.Not: - case ExpressionType.Convert: - case ExpressionType.ConvertChecked: - case ExpressionType.ArrayLength: - case ExpressionType.Quote: - case ExpressionType.TypeAs: - return VisitUnary(exp as UnaryExpression); - case ExpressionType.Parameter: - return VisitParameter(exp as ParameterExpression); - case ExpressionType.Call: - return VisitMethodCall(exp as MethodCallExpression); - case ExpressionType.New: - return VisitNew(exp as NewExpression); - case ExpressionType.NewArrayInit: - case ExpressionType.NewArrayBounds: - return VisitNewArray(exp as NewArrayExpression); - default: - return exp.ToString(); - } - } - - protected virtual string VisitLambda(LambdaExpression lambda) - { - if (lambda.Body.NodeType == ExpressionType.MemberAccess && sep == " ") - { - MemberExpression m = lambda.Body as MemberExpression; - - if (m.Expression != null) - { - string r = VisitMemberAccess(m); - return string.Format("{0}={1}", r, GetQuotedTrueValue()); - } - - } - return Visit(lambda.Body); - } - - protected virtual string VisitBinary(BinaryExpression b) - { - string left, right; - var operand = BindOperant(b.NodeType); //sep= " " ?? - if (operand == "AND" || operand == "OR") - { - MemberExpression m = b.Left as MemberExpression; - if (m != null && m.Expression != null) - { - string r = VisitMemberAccess(m); - left = string.Format("{0}={1}", r, GetQuotedTrueValue()); - } - else - { - left = Visit(b.Left); - } - m = b.Right as MemberExpression; - if (m != null && m.Expression != null) - { - string r = VisitMemberAccess(m); - right = string.Format("{0}={1}", r, GetQuotedTrueValue()); - } - else - { - right = Visit(b.Right); - } - } - else - { - left = Visit(b.Left); - right = Visit(b.Right); - } - - 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 string.Format("{0}({1},{2})", operand, left, right); - default: - return left + sep + operand + sep + right; - } - } - - protected virtual string VisitMemberAccess(MemberExpression m) - { - if (m.Expression != null && - m.Expression.NodeType == ExpressionType.Parameter - && m.Expression.Type == typeof(T)) - { - string field = GetFieldName(pd, m.Member.Name); - return field; - } - - if (m.Expression != null && m.Expression.NodeType != ExpressionType.Constant) - { - Database.Mapper = new ModelDtoMapper(); - var def = new Database.PocoData(m.Expression.Type); - string field = GetFieldName(def, m.Member.Name); - return field; - } - - - var member = Expression.Convert(m, typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - object o = getter(); - return GetQuotedValue(o, o != null ? o.GetType() : null); - - } - - protected virtual string VisitNew(NewExpression nex) - { - // TODO : check ! - var member = Expression.Convert(nex, typeof(object)); - var lambda = Expression.Lambda>(member); - try - { - var getter = lambda.Compile(); - object o = getter(); - return GetQuotedValue(o, o.GetType()); - } - catch (System.InvalidOperationException) - { // FieldName ? - List exprs = VisitExpressionList(nex.Arguments); - var r = new StringBuilder(); - foreach (Object e in exprs) - { - r.AppendFormat("{0}{1}", - r.Length > 0 ? "," : "", - e); - } - return r.ToString(); - } - - } - - protected virtual string VisitParameter(ParameterExpression p) - { - return p.Name; - } - - protected virtual string VisitConstant(ConstantExpression c) - { - if (c.Value == null) - return "null"; - else if (c.Value.GetType() == typeof(bool)) - { - object o = GetQuotedValue(c.Value, c.Value.GetType()); - return string.Format("({0}={1})", GetQuotedTrueValue(), o); - } - else - return GetQuotedValue(c.Value, c.Value.GetType()); - } - - protected virtual string VisitUnary(UnaryExpression u) - { - switch (u.NodeType) - { - case ExpressionType.Not: - string o = Visit(u.Operand); - if (IsFieldName(o)) o = o + "=" + GetQuotedValue(true, typeof(bool)); - return "NOT (" + o + ")"; - default: - return Visit(u.Operand); - } - - } - - protected virtual string VisitMethodCall(MethodCallExpression m) - { - List args = this.VisitExpressionList(m.Arguments); - - Object r; - if (m.Object != null) - r = Visit(m.Object); - else - { - r = args[0]; - args.RemoveAt(0); - } - - switch (m.Method.Name) - { - case "ToUpper": - return string.Format("upper({0})", r); - case "ToLower": - return string.Format("lower({0})", r); - case "StartsWith": - return string.Format("upper({0}) starting with {1} ", r, args[0].ToString().ToUpper()); - case "EndsWith": - return string.Format("upper({0}) like '%{1}'", r, RemoveQuote(args[0].ToString()).ToUpper()); - case "Contains": - return string.Format("upper({0}) like '%{1}%'", r, RemoveQuote(args[0].ToString()).ToUpper()); - 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.ToString()); - - case "In": - - var member = Expression.Convert(m.Arguments[1], typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - - var inArgs = getter() as object[]; - - var sIn = new StringBuilder(); - foreach (Object e in inArgs) - { - if (e.GetType().ToString() != "System.Collections.Generic.List`1[System.Object]") - { - sIn.AppendFormat("{0}{1}", - sIn.Length > 0 ? "," : "", - GetQuotedValue(e, e.GetType())); - } - else - { - var listArgs = e as IList; - foreach (Object el in listArgs) - { - sIn.AppendFormat("{0}{1}", - sIn.Length > 0 ? "," : "", - GetQuotedValue(el, el.GetType())); - } - } - } - - return string.Format("{0} {1} ({2})", r, m.Method.Name, sIn.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 "ToString": - return r.ToString(); - default: - 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()); - } - } - - protected virtual List VisitExpressionList(ReadOnlyCollection original) - { - var list = new 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 VisitNewArray(NewArrayExpression na) - { - - List exprs = VisitExpressionList(na.Expressions); - var r = new StringBuilder(); - foreach (Object e in exprs) - { - r.Append(r.Length > 0 ? "," + e : e); - } - - return r.ToString(); - } - - protected virtual List VisitNewArrayFromExpressionList(NewArrayExpression na) - { - - List exprs = VisitExpressionList(na.Expressions); - return exprs; - } - - - 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(); - } - } - - public virtual string GetQuotedTableName(string tableName) - { - return string.Format("\"{0}\"", tableName); - } - - public virtual string GetQuotedColumnName(string columnName) - { - return string.Format("\"{0}\"", columnName); - } - - public virtual string GetQuotedName(string name) - { - return string.Format("\"{0}\"", name); - } - - private string GetQuotedTrueValue() - { - return GetQuotedValue(true, typeof(bool)); - } - - private string GetQuotedFalseValue() - { - return GetQuotedValue(false, typeof(bool)); - } - - public virtual string GetQuotedValue(object value, Type fieldType) - { - if (value == null) return "NULL"; - - if (!fieldType.UnderlyingSystemType.IsValueType && fieldType != typeof(string)) - { - //if (TypeSerializer.CanCreateFromString(fieldType)) - //{ - // return "'" + EscapeParam(TypeSerializer.SerializeToString(value)) + "'"; - //} - - throw new NotSupportedException( - string.Format("Property of type: {0} is not supported", fieldType.FullName)); - } - - if (fieldType == typeof(int)) - return ((int)value).ToString(CultureInfo.InvariantCulture); - - if (fieldType == typeof(float)) - return ((float)value).ToString(CultureInfo.InvariantCulture); - - if (fieldType == typeof(double)) - return ((double)value).ToString(CultureInfo.InvariantCulture); - - if (fieldType == typeof(decimal)) - return ((decimal)value).ToString(CultureInfo.InvariantCulture); - - return ShouldQuoteValue(fieldType) - ? "'" + EscapeParam(value) + "'" - : value.ToString(); - } - - public virtual string EscapeParam(object paramValue) - { - return paramValue.ToString().Replace("'", "''"); - } - - public virtual bool ShouldQuoteValue(Type fieldType) - { - return true; - } - - protected virtual string GetFieldName(Database.PocoData pocoData, string name) - { - var column = pocoData.Columns.FirstOrDefault(x => x.Value.PropertyInfo.Name == name); - return column.Value.ColumnName; - } - - protected virtual string GetFieldName(string name) - { - - if (useFieldName) - { - //FieldDefinition fd = modelDef.FieldDefinitions.FirstOrDefault(x => x.Name == name); - //string fn = fd != default(FieldDefinition) ? fd.FieldName : name; - //return OrmLiteConfig.DialectProvider.GetQuotedColumnName(fn); - return "[" + name + "]"; - } - else - { - return name; - } - } - - protected string RemoveQuote(string exp) - { - - if (exp.StartsWith("'") && exp.EndsWith("'")) - { - exp = exp.Remove(0, 1); - exp = exp.Remove(exp.Length - 1, 1); - } - return exp; - } - - protected string RemoveQuoteFromAlias(string exp) - { - - if ((exp.StartsWith("\"") || exp.StartsWith("`") || exp.StartsWith("'")) - && - (exp.EndsWith("\"") || exp.EndsWith("`") || exp.EndsWith("'"))) - { - exp = exp.Remove(0, 1); - exp = exp.Remove(exp.Length - 1, 1); - } - return exp; - } - - private string GetTrueExpression() - { - object o = GetQuotedTrueValue(); - return string.Format("({0}={1})", o, o); - } - - private string GetFalseExpression() - { - - return string.Format("({0}={1})", - GetQuotedTrueValue(), - GetQuotedFalseValue()); - } - - private bool IsTrueExpression(string exp) - { - return (exp == GetTrueExpression()); - } - - private bool IsFalseExpression(string exp) - { - return (exp == GetFalseExpression()); - } - - protected bool IsFieldName(string quotedExp) - { - return true; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs index 9a7962230d..ee0c53f023 100644 --- a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs +++ b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs @@ -12,49 +12,6 @@ namespace Umbraco.Core.Persistence.Querying { // fixme.npoco - are we basically duplicating entire parts of NPoco just because of SqlSyntax ?! - /// - /// Represents an expression which caches the visitor's result. - /// - internal class CachedExpression : Expression - { - private string _visitResult; - - /// - /// Gets or sets the inner Expression. - /// - public Expression InnerExpression { get; private set; } - - /// - /// Gets or sets the compiled SQL statement output. - /// - public string VisitResult - { - get { return _visitResult; } - set - { - if (Visited) - throw new InvalidOperationException("Cached expression has already been visited."); - _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; - } - } - /// /// An expression tree parser to create SQL statements and SQL parameters based on a strongly typed expression. /// @@ -584,6 +541,18 @@ namespace Umbraco.Core.Persistence.Querying case "InvariantContains": case "InvariantEquals": + //special case, if it is 'Contains' and the argumet that Contains is being called on is + //Enumerable and the methodArgs is the actual member access, then it's an SQL IN claus + if (m.Object == null + && m.Arguments[0].Type != typeof(string) + && m.Arguments.Count == 2 + && methodArgs.Length == 1 + && methodArgs[0].NodeType == ExpressionType.MemberAccess + && TypeHelper.IsTypeAssignableFrom(m.Arguments[0].Type)) + { + goto case "SqlIn"; + } + string compareValue; if (methodArgs[0].NodeType != ExpressionType.Constant) @@ -600,13 +569,6 @@ namespace Umbraco.Core.Persistence.Querying compareValue = methodArgs[0].ToString(); } - //special case, if it is 'Contains' and the member that Contains is being called on is not a string, then - // we should be doing an 'In' clause - but we currently do not support this - if (methodArgs[0].Type != typeof(string) && TypeHelper.IsTypeAssignableFrom(methodArgs[0].Type)) - { - throw new NotSupportedException("An array Contains method is not supported"); - } - //default column type var colType = TextColumnType.NVarchar; @@ -708,29 +670,33 @@ namespace Umbraco.Core.Persistence.Querying // } // return string.Format("{0}{1}", r, s); - //case "In": + case "SqlIn": - // var member = Expression.Convert(m.Arguments[0], typeof(object)); - // var lambda = Expression.Lambda>(member); - // var getter = lambda.Compile(); + if (m.Object == null && methodArgs.Length == 1 && methodArgs[0].NodeType == ExpressionType.MemberAccess) + { + var memberAccess = VisitMemberAccess((MemberExpression) methodArgs[0]); + + var member = Expression.Convert(m.Arguments[0], typeof(object)); + var lambda = Expression.Lambda>(member); + var getter = lambda.Compile(); - // var inArgs = (object[])getter(); + var inArgs = (IEnumerable)getter(); - // var sIn = new StringBuilder(); - // foreach (var e in inArgs) - // { - // SqlParameters.Add(e); + var sIn = new StringBuilder(); + foreach (var e in inArgs) + { + SqlParameters.Add(e); - // sIn.AppendFormat("{0}{1}", - // sIn.Length > 0 ? "," : "", - // string.Format("@{0}", SqlParameters.Count - 1)); + sIn.AppendFormat("{0}{1}", + sIn.Length > 0 ? "," : "", + string.Format("@{0}", SqlParameters.Count - 1)); + } - // //sIn.AppendFormat("{0}{1}", - // // sIn.Length > 0 ? "," : "", - // // GetQuotedValue(e, e.GetType())); - // } + return string.Format("{0} IN ({1})", memberAccess, sIn); + } + + throw new NotSupportedException("SqlIn must contain the member being accessed"); - // return string.Format("{0} {1} ({2})", r, m.Method.Name, sIn.ToString()); //case "Desc": // return string.Format("{0} DESC", r); //case "Alias": diff --git a/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionVisitor.cs b/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionVisitor.cs index 94ac8444a4..b786e201d0 100644 --- a/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionVisitor.cs +++ b/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionVisitor.cs @@ -1,7 +1,9 @@ using System; using System.Linq.Expressions; +using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Plugins; namespace Umbraco.Core.Persistence.Querying { @@ -12,18 +14,13 @@ namespace Umbraco.Core.Persistence.Querying /// This object is stateful and cannot be re-used to parse an expression. internal class ModelToSqlExpressionVisitor : ExpressionVisitorBase { + private readonly IMapperCollection _mappers; private readonly BaseMapper _mapper; - public ModelToSqlExpressionVisitor(ISqlSyntaxProvider sqlSyntax, BaseMapper mapper) - : base(sqlSyntax) - { - if (mapper == null) throw new ArgumentNullException(nameof(mapper)); - _mapper = mapper; - } - public ModelToSqlExpressionVisitor(ISqlSyntaxProvider sqlSyntax, IMapperCollection mappers) : base(sqlSyntax) { + _mappers = mappers; _mapper = mappers[typeof(T)]; // throws if not found } @@ -38,7 +35,7 @@ namespace Umbraco.Core.Persistence.Querying { var field = _mapper.Map(SqlSyntax, m.Member.Name, true); if (field.IsNullOrWhiteSpace()) - throw new InvalidOperationException("The mapper returned an empty field for the member name: " + m.Member.Name); + throw new InvalidOperationException($"The mapper returned an empty field for the member name: {m.Member.Name} for type: {m.Expression.Type}."); return field; } @@ -53,7 +50,7 @@ namespace Umbraco.Core.Persistence.Querying { var field = _mapper.Map(SqlSyntax, m.Member.Name, true); if (field.IsNullOrWhiteSpace()) - throw new InvalidOperationException("The mapper returned an empty field for the member name: " + m.Member.Name); + throw new InvalidOperationException($"The mapper returned an empty field for the member name: {m.Member.Name} for type: {m.Expression.Type}."); return field; } @@ -61,6 +58,33 @@ namespace Umbraco.Core.Persistence.Querying return string.Empty; } + if (m.Expression != null + && m.Expression.Type != typeof(T) + && TypeHelper.IsTypeAssignableFrom(m.Expression.Type) + && EndsWithConstant(m) == false) + { + //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 subMapper = _mappers[m.Expression.Type]; // throws if not found + var field = subMapper.Map(SqlSyntax, m.Member.Name, true); + 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. + var member = Expression.Convert(m, typeof(object)); var lambda = Expression.Lambda>(member); var getter = lambda.Compile(); @@ -74,7 +98,25 @@ namespace Umbraco.Core.Persistence.Querying //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; + expr = memberExpr.Expression; + } + + var constExpr = expr as ConstantExpression; + return constExpr != null; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Querying/SqlStringExtensions.cs b/src/Umbraco.Core/Persistence/Querying/SqlExpressionExtensions.cs similarity index 83% rename from src/Umbraco.Core/Persistence/Querying/SqlStringExtensions.cs rename to src/Umbraco.Core/Persistence/Querying/SqlExpressionExtensions.cs index cbecc0a591..593955734e 100644 --- a/src/Umbraco.Core/Persistence/Querying/SqlStringExtensions.cs +++ b/src/Umbraco.Core/Persistence/Querying/SqlExpressionExtensions.cs @@ -1,4 +1,5 @@ -using System; +using System.Collections.Generic; +using System.Linq; using System.Text.RegularExpressions; namespace Umbraco.Core.Persistence.Querying @@ -6,8 +7,13 @@ namespace Umbraco.Core.Persistence.Querying /// /// String extension methods used specifically to translate into SQL /// - internal static class SqlStringExtensions + internal static class SqlExpressionExtensions { + public static bool SqlIn(this IEnumerable collection, T item) + { + return collection.Contains(item); + } + public static bool SqlWildcard(this string str, string txt, TextColumnType columnType) { var wildcardmatch = new Regex("^" + Regex.Escape(txt). diff --git a/src/Umbraco.Core/Persistence/Repositories/AuditRepository.cs b/src/Umbraco.Core/Persistence/Repositories/AuditRepository.cs index 0d825ab08c..eeff23c877 100644 --- a/src/Umbraco.Core/Persistence/Repositories/AuditRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/AuditRepository.cs @@ -15,7 +15,7 @@ namespace Umbraco.Core.Persistence.Repositories { internal class AuditRepository : NPocoRepositoryBase, IAuditRepository { - public AuditRepository(IDatabaseUnitOfWork work, [Inject(RepositoryCompositionRoot.DisabledCache)] CacheHelper cache, ILogger logger) + public AuditRepository(IScopeUnitOfWork work, [Inject(RepositoryCompositionRoot.DisabledCache)] CacheHelper cache, ILogger logger) : base(work, cache, logger) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 9ab2e0b525..7cbd3f7570 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -32,15 +32,12 @@ namespace Umbraco.Core.Persistence.Repositories private readonly CacheHelper _cacheHelper; private PermissionRepository _permissionRepository; - public ContentRepository(IDatabaseUnitOfWork work, CacheHelper cacheHelper, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, IContentSection contentSection) - : base(work, cacheHelper, logger, contentSection) + public ContentRepository(IScopeUnitOfWork work, CacheHelper cacheHelper, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository /*, IContentSection contentSection*/) + : base(work, cacheHelper, logger /*, contentSection*/) { - if (contentTypeRepository == null) throw new ArgumentNullException(nameof(contentTypeRepository)); - if (templateRepository == null) throw new ArgumentNullException(nameof(templateRepository)); - if (tagRepository == null) throw new ArgumentNullException(nameof(tagRepository)); - _contentTypeRepository = contentTypeRepository; - _templateRepository = templateRepository; - _tagRepository = tagRepository; + _contentTypeRepository = contentTypeRepository ?? throw new ArgumentNullException(nameof(contentTypeRepository)); + _templateRepository = templateRepository ?? throw new ArgumentNullException(nameof(templateRepository)); + _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); _cacheHelper = cacheHelper; _publishedQuery = work.Query().Where(x => x.Published); @@ -48,14 +45,7 @@ namespace Umbraco.Core.Persistence.Repositories EnsureUniqueNaming = true; } - public void SetNoCachePolicy() - { - // using NoCache here means that we are NOT updating the cache - // so this should be OK for reads but NOT for writes! - CachePolicy = new NoCacheRepositoryCachePolicy(); - } - - protected override ContentRepository Instance => this; + protected override ContentRepository This => this; public bool EnsureUniqueNaming { get; set; } @@ -67,7 +57,7 @@ namespace Umbraco.Core.Persistence.Repositories protected override IContent PerformGet(int id) { - var sql = GetBaseQuery(false) + var sql = GetBaseQuery(QueryType.Single) .Where(GetBaseWhereClause(), new { Id = id }) .Where(x => x.Newest) .OrderByDescending(x => x.VersionDate); @@ -84,21 +74,24 @@ namespace Umbraco.Core.Persistence.Repositories protected override IEnumerable PerformGetAll(params int[] ids) { - var sql = GetBaseQuery(false); - if (ids.Any()) + Sql Translate(Sql tsql) { - sql.Where("umbracoNode.id in (@ids)", new { /*ids =*/ ids }); + if (ids.Any()) + tsql.Where("umbracoNode.id in (@ids)", new { /*ids =*/ ids }); + + // we only want the newest ones with this method + tsql.Where(x => x.Newest); + + return tsql; } - //we only want the newest ones with this method - sql.Where(x => x.Newest); - - return MapQueryDtos(Database.Fetch(sql)); + var sql = Translate(GetBaseQuery(QueryType.Many)); + return MapQueryDtos(Database.Fetch(sql), many: true); } protected override IEnumerable PerformGetByQuery(IQuery query) { - var sqlClause = GetBaseQuery(false); + var sqlClause = GetBaseQuery(QueryType.Many); var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate() .Where(x => x.Newest) @@ -106,7 +99,7 @@ namespace Umbraco.Core.Persistence.Repositories .OrderBy(x => x.Level) .OrderBy(x => x.SortOrder); - return MapQueryDtos(Database.Fetch(sql)); + return MapQueryDtos(Database.Fetch(sql), many: true); } #endregion @@ -119,43 +112,78 @@ namespace Umbraco.Core.Persistence.Repositories #region Overrides of NPocoRepositoryBase - protected override Sql GetBaseQuery(bool isCount) + protected override Sql GetBaseQuery(QueryType queryType) { - var sqlx = string.Format("LEFT OUTER JOIN {0} {1} ON ({1}.{2}={0}.{2} AND {1}.{3}=1)", - SqlSyntax.GetQuotedTableName("cmsDocument"), - SqlSyntax.GetQuotedTableName("cmsDocument2"), - SqlSyntax.GetQuotedColumnName("nodeId"), - SqlSyntax.GetQuotedColumnName("published")); - var sql = Sql(); - sql = isCount - ? sql.SelectCount() - : sql.Select(r => - r.Select(rr => - rr.Select(rrr => - rrr.Select())) - .Select(tableAlias: "cmsDocument2")); + switch (queryType) + { + case QueryType.Count: + sql = sql.SelectCount(); + break; + case QueryType.Ids: + sql = sql.Select("cmsDocument.nodeId"); + break; + case QueryType.Single: + sql = sql.Select(r => + r.Select(rr => + rr.Select(rrr => + rrr.Select())) + .Select(tableAlias: "cmsDocument2")); + break; + case QueryType.Many: + // 'many' does not join on cmsDocument2 + sql = sql.Select(r => + r.Select(rr => + rr.Select(rrr => + rrr.Select()))); + break; + } sql .From() .InnerJoin() - .On(left => left.VersionId, right => right.VersionId) + .On(left => left.VersionId, right => right.VersionId) .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) + .On(left => left.NodeId, right => right.NodeId) .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) + .On(left => left.NodeId, right => right.NodeId); + + if (queryType == QueryType.Single) + { + //The only reason we apply this left outer join is to be able to pull back the DocumentPublishedReadOnlyDto + //information with the entire data set, so basically this will get both the latest document and also it's published + //version if it has one. When performing a count or when retrieving Ids like in paging, this is unecessary + //and causes huge performance overhead for the SQL server, especially when sorting the result. + //We also don't include this outer join when querying for multiple entities since it is much faster to fetch this information + //in a separate query. For a single entity this is ok. + + var sqlx = string.Format("LEFT OUTER JOIN {0} {1} ON ({1}.{2}={0}.{2} AND {1}.{3}=1)", + SqlSyntax.GetQuotedTableName("cmsDocument"), + SqlSyntax.GetQuotedTableName("cmsDocument2"), + SqlSyntax.GetQuotedColumnName("nodeId"), + SqlSyntax.GetQuotedColumnName("published")); // cannot do this because NPoco does not know how to alias the table //.LeftOuterJoin() //.On(left => left.NodeId, right => right.NodeId) // so have to rely on writing our own SQL - .Append(sqlx/*, new { @published = true }*/) + sql + .Append(sqlx /*, new { @published = true }*/); + } + sql .Where(x => x.NodeObjectType == NodeObjectTypeId); + return sql; } + // fixme - move that one up to Versionable! + protected override Sql GetBaseQuery(bool isCount) + { + return GetBaseQuery(isCount ? QueryType.Count : QueryType.Single); + } + protected override string GetBaseWhereClause() { return "umbracoNode.id = @Id"; @@ -304,10 +332,11 @@ namespace Umbraco.Core.Persistence.Repositories 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); + var unused = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); //Update with new correct path nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + nodeDto.ValidatePathWithException(); Database.Update(nodeDto); //Update entity with correct values @@ -376,11 +405,13 @@ namespace Umbraco.Core.Persistence.Repositories dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto { VersionId = dto.VersionId, + VersionDate = dto.UpdateDate, Newest = true, NodeId = dto.NodeId, Published = true }; - ((Content)entity).PublishedVersionGuid = dto.VersionId; + ((Content) entity).PublishedVersionGuid = dto.VersionId; + ((Content) entity).PublishedDate = dto.UpdateDate; } OnUowRefreshedEntity(new UnitOfWorkEntityEventArgs(UnitOfWork, entity)); @@ -441,7 +472,8 @@ namespace Umbraco.Core.Persistence.Repositories //Updates the (base) node data - umbracoNode var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; - var o = Database.Update(nodeDto); + nodeDto.ValidatePathWithException(); + var unused = Database.Update(nodeDto); //Only update this DTO if the contentType has actually changed if (contentDto.ContentTypeId != entity.ContentTypeId) @@ -547,22 +579,26 @@ namespace Umbraco.Core.Persistence.Repositories dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto { VersionId = dto.VersionId, + VersionDate = dto.UpdateDate, Newest = true, NodeId = dto.NodeId, Published = true }; content.PublishedVersionGuid = dto.VersionId; + content.PublishedDate = dto.UpdateDate; } else if (publishedStateChanged) { dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto { VersionId = default(Guid), + VersionDate = default (DateTime), Newest = false, NodeId = dto.NodeId, Published = false }; content.PublishedVersionGuid = default(Guid); + content.PublishedDate = dto.UpdateDate; } OnUowRefreshedEntity(new UnitOfWorkEntityEventArgs(UnitOfWork, entity)); @@ -588,14 +624,14 @@ namespace Umbraco.Core.Persistence.Repositories // we WANT to return contents in top-down order, ie parents should come before children // ideal would be pure xml "document order" - which we cannot achieve at database level - var sqlClause = GetBaseQuery(false); + var sqlClause = GetBaseQuery(QueryType.Many); var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate() .Where(x => x.Published) .OrderBy(x => x.Level) .OrderBy(x => x.SortOrder); - return MapQueryDtos(Database.Fetch(sql), true); + return MapQueryDtos(Database.Fetch(sql), true, many: true); } public int CountPublished(string contentTypeAlias = null) @@ -635,24 +671,14 @@ namespace Umbraco.Core.Persistence.Repositories public void ClearPublishedFlag(IContent content) { - // no race cond if locked - var documentDtos = Database.Fetch("WHERE nodeId=@Id AND published=@IsPublished", new { /*Id =*/ content.Id, IsPublished = true }); - foreach (var documentDto in documentDtos) - { - documentDto.Published = false; - Database.Update(documentDto); - } + var sql = "UPDATE cmsDocument SET published=0 WHERE nodeId=@id AND published=1"; + Database.Execute(sql, new { id = content.Id }); } public void ClearNewestFlag(IContent content) { - // no race cond if locked - var documentDtos = Database.Fetch("WHERE nodeId=@Id AND newest=@IsNewest", new { /*Id =*/ content.Id, IsNewest = true }); - foreach (var documentDto in documentDtos) - { - documentDto.Newest = false; - Database.Update(documentDto); - } + var sql = "UPDATE cmsDocument SET newest=0 WHERE nodeId=@id AND newest=1"; + Database.Execute(sql, new { id = content.Id }); } /// @@ -697,7 +723,7 @@ namespace Umbraco.Core.Persistence.Repositories } return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, - x => MapQueryDtos(x), + x => MapQueryDtos(x, many: true), orderBy, orderDirection, orderBySystemField, "cmsDocument", filterSql); } @@ -759,22 +785,45 @@ WHERE (@path LIKE {5})", return base.GetDatabaseFieldNameForOrderBy(orderBy); } - private IEnumerable MapQueryDtos(List dtos, bool withCache = false) + // fixme - missing the 'include all versions' thing here - see 7.6 ProcessQuery + // + private IEnumerable MapQueryDtos(List dtos, bool withCache = false, bool many = false) { - //nothing found var content = new IContent[dtos.Count]; - var defs = new List(); + var temps = new List(); + var contentTypes = new Dictionary(); var templateIds = new List(); + // populate published data + if (many) + { + var publishedDtoIndex = Database.FetchByGroups(dtos.Select(x => x.NodeId), 2000, batch + => Sql() + .Select() + .From() + .WhereIn(x => x.NodeId, batch) + .Where(x => x.Published)) + .ToDictionary(x => x.NodeId, x => x); + + foreach (var dto in dtos) + { + if (publishedDtoIndex.TryGetValue(dto.NodeId, out DocumentPublishedReadOnlyDto d) == false) + d = new DocumentPublishedReadOnlyDto(); + dto.DocumentPublishedReadOnlyDto = d; + } + } + for (var i = 0; i < dtos.Count; i++) { var dto = dtos[i]; - // if the cache contains the published version, use it if (withCache) { - var cached = RuntimeCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); + // if the cache contains the (proper version of the) item, use it + var cached = IsolatedCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); + // fixme - wtf? only published? if (cached != null && cached.Published) + //if (cached != null && cached.Version == dto.ContentVersionDto.VersionId) { content[i] = cached; continue; @@ -782,51 +831,49 @@ WHERE (@path LIKE {5})", } // else, need to fetch from the database - // content type repository is full-cache so OK to get each one independently - var contentType = _contentTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); - var factory = new ContentFactory(contentType, NodeObjectTypeId, dto.NodeId); - content[i] = factory.BuildEntity(dto); + + // 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 + if (contentTypes.TryGetValue(dto.ContentVersionDto.ContentDto.ContentTypeId, out IContentType contentType) == false) + contentTypes[dto.ContentVersionDto.ContentDto.ContentTypeId] = contentType = _contentTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); + + var c = content[i] = ContentFactory.BuildEntity(dto, contentType, dto.DocumentPublishedReadOnlyDto); // need template if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0) templateIds.Add(dto.TemplateId.Value); // need properties - defs.Add(new DocumentDefinition( + temps.Add(new TempContent( dto.NodeId, dto.VersionId, dto.ContentVersionDto.VersionDate, dto.ContentVersionDto.ContentDto.NodeDto.CreateDate, - contentType - )); + contentType, + c + ) { TemplateId = dto.TemplateId }); } - // load all required templates in 1 query + // load all required templates in 1 query and index var templates = _templateRepository.GetAll(templateIds.ToArray()) .ToDictionary(x => x.Id, x => x); // load all properties for all documents from database in 1 query - var propertyData = GetPropertyCollection(defs.ToArray()); + var propertyData = GetPropertyCollection(temps); // assign - var dtoIndex = 0; - foreach (var def in defs) + foreach (var temp in temps) { - // move to corresponding item (which has to exist) - while (dtos[dtoIndex].NodeId != def.Id) dtoIndex++; - // complete the item - var cc = content[dtoIndex]; - var dto = dtos[dtoIndex]; ITemplate template = null; - if (dto.TemplateId.HasValue) - templates.TryGetValue(dto.TemplateId.Value, out template); // else null - cc.Template = template; - cc.Properties = propertyData[cc.Id]; + if (temp.TemplateId.HasValue) + templates.TryGetValue(temp.TemplateId.Value, out template); // else null + ((Content) temp.Content).Template = template; + temp.Content.Properties = propertyData[temp.Version]; //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 - ((Entity) cc).ResetDirtyProperties(false); + ((Entity) temp.Content).ResetDirtyProperties(false); } return content; @@ -851,11 +898,11 @@ WHERE (@path LIKE {5})", content.Template = _templateRepository.Get(dto.TemplateId.Value); } - var docDef = new DocumentDefinition(dto.NodeId, versionId, content.UpdateDate, content.CreateDate, contentType); + var docDef = new TempContent(dto.NodeId, versionId, content.UpdateDate, content.CreateDate, contentType); - var properties = GetPropertyCollection(new[] { docDef }); + var properties = GetPropertyCollection(new List { docDef }); - content.Properties = properties[dto.NodeId]; + content.Properties = properties[versionId]; //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs index f561b85f31..960ed8a701 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs @@ -22,22 +22,15 @@ namespace Umbraco.Core.Persistence.Repositories private readonly ITemplateRepository _templateRepository; private IRepositoryCachePolicy _cachePolicy; - public ContentTypeRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ITemplateRepository templateRepository) + public ContentTypeRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ITemplateRepository templateRepository) : base(work, cache, logger) { _templateRepository = templateRepository; } - protected override IRepositoryCachePolicy CachePolicy + protected override IRepositoryCachePolicy CreateCachePolicy(IRuntimeCacheProvider runtimeCache) { - get - { - if (_cachePolicy != null) return _cachePolicy; - - _cachePolicy = new FullDataSetRepositoryCachePolicy(RuntimeCache, GetEntityId, /*expires:*/ true); - - return _cachePolicy; - } + return new FullDataSetRepositoryCachePolicy(runtimeCache, GetEntityId, /*expires:*/ true); } protected override IContentType PerformGet(int id) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepositoryBase.cs index 7f2e3329e7..dd841752cf 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepositoryBase.cs @@ -30,7 +30,7 @@ namespace Umbraco.Core.Persistence.Repositories internal abstract class ContentTypeRepositoryBase : NPocoRepositoryBase, IReadRepository where TEntity : class, IContentTypeComposition { - protected ContentTypeRepositoryBase(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger) + protected ContentTypeRepositoryBase(IScopeUnitOfWork work, CacheHelper cache, ILogger logger) : base(work, cache, logger) { } @@ -103,8 +103,9 @@ namespace Umbraco.Core.Persistence.Repositories .On(left => left.DataTypeId, right => right.DataTypeId); var translator = new SqlTranslator(sqlClause, query); + // fixme v8 are we sorting only for 7.6 relators? var sql = translator.Translate() - .OrderBy(x => x.PropertyTypeGroupId); + .OrderBy(x => x.PropertyTypeGroupId); return Database .FetchOneToMany(x => x.PropertyTypeDtos, sql) @@ -1272,6 +1273,20 @@ WHERE cmsContentType." + aliasColumn + @" LIKE @pattern", return test; } + /// + /// Given the path of a content item, this will return true if the content item exists underneath a list view content item + /// + /// + /// + public bool HasContainerInPath(string contentPath) + { + var ids = contentPath.Split(',').Select(int.Parse); + var sql = new Sql(@"SELECT COUNT(*) FROM cmsContentType +INNER JOIN cmsContent ON cmsContentType.nodeId=cmsContent.contentType +WHERE cmsContent.nodeId IN (@ids) AND cmsContentType.isContainer=@isContainer", new { ids, isContainer = true }); + return Database.ExecuteScalar(sql) > 0; + } + protected override IEnumerable GetDeleteClauses() { // in theory, services should have ensured that content items of the given content type diff --git a/src/Umbraco.Core/Persistence/Repositories/DataTypeContainerRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DataTypeContainerRepository.cs index d50e3a5092..0b0d6d4aa5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DataTypeContainerRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DataTypeContainerRepository.cs @@ -1,13 +1,12 @@ using Umbraco.Core.Cache; using Umbraco.Core.Logging; -using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { class DataTypeContainerRepository : EntityContainerRepository, IDataTypeContainerRepository { - public DataTypeContainerRepository(IDatabaseUnitOfWork uow, CacheHelper cache, ILogger logger) + public DataTypeContainerRepository(IScopeUnitOfWork uow, CacheHelper cache, ILogger logger) : base(uow, cache, logger, Constants.ObjectTypes.DataTypeContainerGuid) { } } diff --git a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs index a345717141..65708ef299 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs @@ -31,11 +31,11 @@ namespace Umbraco.Core.Persistence.Repositories private readonly IContentTypeRepository _contentTypeRepository; private readonly DataTypePreValueRepository _preValRepository; - public DataTypeDefinitionRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, IContentTypeRepository contentTypeRepository) + public DataTypeDefinitionRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, IContentTypeRepository contentTypeRepository) : base(work, cache, logger) { _contentTypeRepository = contentTypeRepository; - _preValRepository = new DataTypePreValueRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger); + _preValRepository = new DataTypePreValueRepository(work, CacheHelper.NoCache, logger); } #region Overrides of RepositoryBase @@ -52,7 +52,7 @@ namespace Umbraco.Core.Persistence.Repositories if (ids.Any()) { - dataTypeSql.Where("umbracoNode.id in (@ids)", new { ids = ids }); + dataTypeSql.Where("umbracoNode.id in (@ids)", new { ids }); } else { @@ -252,7 +252,7 @@ AND umbracoNode.id <> @id", //NOTE: This is a special case, we need to clear the custom cache for pre-values here so they are not stale if devs // are querying for them in the Saved event (before the distributed call cache is clearing it) - RuntimeCache.ClearCacheItem(GetPrefixedCacheKey(entity.Id)); + IsolatedCache.ClearCacheItem(GetPrefixedCacheKey(entity.Id)); entity.ResetDirtyProperties(); } @@ -291,14 +291,7 @@ AND umbracoNode.id <> @id", public PreValueCollection GetPreValuesCollectionByDataTypeId(int dataTypeId) { - var cached = RuntimeCache.GetCacheItemsByKeySearch(GetPrefixedCacheKey(dataTypeId)); - if (cached != null && cached.Any()) - { - //return from the cache, ensure it's a cloned result - return (PreValueCollection)cached.First().DeepClone(); - } - - return GetAndCachePreValueCollection(dataTypeId); + return GetCachedPreValueCollection(dataTypeId); } internal static string GetCacheKeyRegex(int preValueId) @@ -308,30 +301,22 @@ AND umbracoNode.id <> @id", public string GetPreValueAsString(int preValueId) { - //We need to see if we can find the cached PreValueCollection based on the cache key above + var collections = IsolatedCache.GetCacheItemsByKeySearch(CacheKeys.DataTypePreValuesCacheKey + "_"); - var cached = RuntimeCache.GetCacheItemsByKeyExpression(GetCacheKeyRegex(preValueId)); - if (cached != null && cached.Any()) - { - //return from the cache - var collection = cached.First(); - var preVal = collection.FormatAsDictionary().Single(x => x.Value.Id == preValueId); - return preVal.Value.Value; - } + var preValue = collections.SelectMany(x => x.FormatAsDictionary().Values).FirstOrDefault(x => x.Id == preValueId); + if (preValue != null) + return preValue.Value; - //go and find the data type id for the pre val id passed in - - var dto = Database.FirstOrDefault("WHERE id = @preValueId", new { preValueId = preValueId }); + var dto = Database.FirstOrDefault("WHERE id = @preValueId", new { preValueId }); if (dto == null) - { return string.Empty; - } - // go cache the collection - var preVals = GetAndCachePreValueCollection(dto.DataTypeNodeId); - //return the single value for this id - var pv = preVals.FormatAsDictionary().Single(x => x.Value.Id == preValueId); - return pv.Value.Value; + var collection = GetCachedPreValueCollection(dto.DataTypeNodeId); + if (collection == null) + return string.Empty; + + preValue = collection.FormatAsDictionary().Values.FirstOrDefault(x => x.Id == preValueId); + return preValue == null ? string.Empty : preValue.Value; } public void AddOrUpdatePreValues(int dataTypeId, IDictionary values) @@ -460,32 +445,21 @@ AND umbracoNode.id <> @id", } - private string GetPrefixedCacheKey(int dataTypeId) + private static string GetPrefixedCacheKey(int dataTypeId) { - return CacheKeys.DataTypePreValuesCacheKey + dataTypeId + "-"; + return CacheKeys.DataTypePreValuesCacheKey + "_" + dataTypeId; } - private PreValueCollection GetAndCachePreValueCollection(int dataTypeId) + private PreValueCollection GetCachedPreValueCollection(int dataTypeId) { - //go get the data - var dtos = Database.Fetch("WHERE datatypeNodeId = @Id", new { Id = dataTypeId }); - var list = dtos.Select(x => new Tuple(new PreValue(x.Id, x.Value, x.SortOrder), x.Alias, x.SortOrder)).ToList(); - var collection = PreValueConverter.ConvertToPreValuesCollection(list); - - //now create the cache key, this needs to include all pre-value ids so that we can use this cached item in the GetPreValuesAsString method - //the key will be: "UmbracoPreValDATATYPEID-CSVOFPREVALIDS - - var key = GetPrefixedCacheKey(dataTypeId) - + string.Join(",", collection.FormatAsDictionary().Select(x => x.Value.Id).ToArray()); - - //store into cache - RuntimeCache.InsertCacheItem(key, () => collection, - //30 mins - new TimeSpan(0, 0, 30), - //sliding is true - true); - - return collection; + var key = GetPrefixedCacheKey(dataTypeId); + return IsolatedCache.GetCacheItem(key, () => + { + var dtos = Database.Fetch("WHERE datatypeNodeId = @Id", new { Id = dataTypeId }); + var list = dtos.Select(x => new Tuple(new PreValue(x.Id, x.Value, x.SortOrder), x.Alias, x.SortOrder)).ToList(); + var collection = PreValueConverter.ConvertToPreValuesCollection(list); + return collection; + }, TimeSpan.FromMinutes(20), isSliding: true); } private string EnsureUniqueNodeName(string nodeName, int id = 0) @@ -533,10 +507,9 @@ AND umbracoNode.id <> @id", /// private class DataTypePreValueRepository : NPocoRepositoryBase { - public DataTypePreValueRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger) + public DataTypePreValueRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger) : base(work, cache, logger) - { - } + { } #region Not implemented (don't need to for the purposes of this repo) protected override PreValueEntity PerformGet(int id) @@ -577,9 +550,7 @@ AND umbracoNode.id <> @id", protected override void PersistDeletedItem(PreValueEntity entity) { - Database.Execute( - "DELETE FROM cmsDataTypePreValues WHERE id=@Id", - new { Id = entity.Id }); + Database.Execute("DELETE FROM cmsDataTypePreValues WHERE id=@Id", new { Id = entity.Id }); } protected override void PersistNewItem(PreValueEntity entity) diff --git a/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs index a8b8bce9b4..129c9922eb 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs @@ -20,28 +20,19 @@ namespace Umbraco.Core.Persistence.Repositories /// internal class DictionaryRepository : NPocoRepositoryBase, IDictionaryRepository { - private IRepositoryCachePolicy _cachePolicy; - - public DictionaryRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger) + public DictionaryRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger) : base(work, cache, logger) { } - protected override IRepositoryCachePolicy CachePolicy + protected override IRepositoryCachePolicy CreateCachePolicy(IRuntimeCacheProvider runtimeCache) { - get + var options = new RepositoryCachePolicyOptions { - if (_cachePolicy != null) return _cachePolicy; + //allow zero to be cached + GetAllCacheAllowZeroCount = true + }; - var options = new RepositoryCachePolicyOptions - { - //allow zero to be cached - GetAllCacheAllowZeroCount = true - }; - - _cachePolicy = new SingleItemsOnlyRepositoryCachePolicy(RuntimeCache, options); - - return _cachePolicy; - } + return new SingleItemsOnlyRepositoryCachePolicy(runtimeCache, options); } #region Overrides of RepositoryBase @@ -190,8 +181,8 @@ namespace Umbraco.Core.Persistence.Repositories entity.ResetDirtyProperties(); //Clear the cache entries that exist by uniqueid/item key - RuntimeCache.ClearCacheItem(GetCacheIdKey(entity.ItemKey)); - RuntimeCache.ClearCacheItem(GetCacheIdKey(entity.Key)); + IsolatedCache.ClearCacheItem(GetCacheIdKey(entity.ItemKey)); + IsolatedCache.ClearCacheItem(GetCacheIdKey(entity.Key)); } protected override void PersistDeletedItem(IDictionaryItem entity) @@ -202,8 +193,8 @@ namespace Umbraco.Core.Persistence.Repositories Database.Delete("WHERE id = @Id", new { Id = entity.Key }); //Clear the cache entries that exist by uniqueid/item key - RuntimeCache.ClearCacheItem(GetCacheIdKey(entity.ItemKey)); - RuntimeCache.ClearCacheItem(GetCacheIdKey(entity.Key)); + IsolatedCache.ClearCacheItem(GetCacheIdKey(entity.ItemKey)); + IsolatedCache.ClearCacheItem(GetCacheIdKey(entity.Key)); } private void RecursiveDelete(Guid parentId) @@ -217,8 +208,8 @@ namespace Umbraco.Core.Persistence.Repositories Database.Delete("WHERE id = @Id", new { Id = dto.UniqueId }); //Clear the cache entries that exist by uniqueid/item key - RuntimeCache.ClearCacheItem(GetCacheIdKey(dto.Key)); - RuntimeCache.ClearCacheItem(GetCacheIdKey(dto.UniqueId)); + IsolatedCache.ClearCacheItem(GetCacheIdKey(dto.Key)); + IsolatedCache.ClearCacheItem(GetCacheIdKey(dto.UniqueId)); } } @@ -240,13 +231,13 @@ namespace Umbraco.Core.Persistence.Repositories public IDictionaryItem Get(Guid uniqueId) { - var uniqueIdRepo = new DictionaryByUniqueIdRepository(this, UnitOfWork, Cache, Logger); + var uniqueIdRepo = new DictionaryByUniqueIdRepository(this, UnitOfWork, GlobalCache, Logger); return uniqueIdRepo.Get(uniqueId); } public IDictionaryItem Get(string key) { - var keyRepo = new DictionaryByKeyRepository(this, UnitOfWork, Cache, Logger); + var keyRepo = new DictionaryByKeyRepository(this, UnitOfWork, GlobalCache, Logger); return keyRepo.Get(key); } @@ -294,7 +285,7 @@ namespace Umbraco.Core.Persistence.Repositories private IRepositoryCachePolicy _cachePolicy; private readonly DictionaryRepository _dictionaryRepository; - public DictionaryByUniqueIdRepository(DictionaryRepository dictionaryRepository, IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger) + public DictionaryByUniqueIdRepository(DictionaryRepository dictionaryRepository, IScopeUnitOfWork work, CacheHelper cache, ILogger logger) : base(work, cache, logger) { _dictionaryRepository = dictionaryRepository; @@ -331,22 +322,15 @@ namespace Umbraco.Core.Persistence.Repositories return "cmsDictionary." + SqlSyntax.GetQuotedColumnName("id") + " in (@ids)"; } - protected override IRepositoryCachePolicy CachePolicy + protected override IRepositoryCachePolicy CreateCachePolicy(IRuntimeCacheProvider runtimeCache) { - get + var options = new RepositoryCachePolicyOptions { - if (_cachePolicy != null) return _cachePolicy; + //allow zero to be cached + GetAllCacheAllowZeroCount = true + }; - var options = new RepositoryCachePolicyOptions - { - //allow zero to be cached - GetAllCacheAllowZeroCount = true - }; - - _cachePolicy = new SingleItemsOnlyRepositoryCachePolicy(RuntimeCache, options); - - return _cachePolicy; - } + return new SingleItemsOnlyRepositoryCachePolicy(runtimeCache, options); } } @@ -355,7 +339,7 @@ namespace Umbraco.Core.Persistence.Repositories private IRepositoryCachePolicy _cachePolicy; private readonly DictionaryRepository _dictionaryRepository; - public DictionaryByKeyRepository(DictionaryRepository dictionaryRepository, IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger) + public DictionaryByKeyRepository(DictionaryRepository dictionaryRepository, IScopeUnitOfWork work, CacheHelper cache, ILogger logger) : base(work, cache, logger) { _dictionaryRepository = dictionaryRepository; @@ -392,22 +376,15 @@ namespace Umbraco.Core.Persistence.Repositories return "cmsDictionary." + SqlSyntax.GetQuotedColumnName("key") + " in (@ids)"; } - protected override IRepositoryCachePolicy CachePolicy + protected override IRepositoryCachePolicy CreateCachePolicy(IRuntimeCacheProvider runtimeCache) { - get + var options = new RepositoryCachePolicyOptions { - if (_cachePolicy != null) return _cachePolicy; + //allow zero to be cached + GetAllCacheAllowZeroCount = true + }; - var options = new RepositoryCachePolicyOptions - { - //allow zero to be cached - GetAllCacheAllowZeroCount = true - }; - - _cachePolicy = new SingleItemsOnlyRepositoryCachePolicy(RuntimeCache, options); - - return _cachePolicy; - } + return new SingleItemsOnlyRepositoryCachePolicy(runtimeCache, options); } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/DocumentTypeContainerRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DocumentTypeContainerRepository.cs index 419f8c0bc1..fc1f1690a9 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DocumentTypeContainerRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DocumentTypeContainerRepository.cs @@ -1,13 +1,12 @@ using Umbraco.Core.Cache; using Umbraco.Core.Logging; -using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { class DocumentTypeContainerRepository : EntityContainerRepository, IDocumentTypeContainerRepository { - public DocumentTypeContainerRepository(IDatabaseUnitOfWork uow, CacheHelper cache, ILogger logger) + public DocumentTypeContainerRepository(IScopeUnitOfWork uow, CacheHelper cache, ILogger logger) : base(uow, cache, logger, Constants.ObjectTypes.DocumentTypeContainerGuid) { } } diff --git a/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs index c4fe1bab8d..8ade06546c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs @@ -18,21 +18,13 @@ namespace Umbraco.Core.Persistence.Repositories { private IRepositoryCachePolicy _cachePolicy; - public DomainRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger) + public DomainRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger) : base(work, cache, logger) - { - } + { } - protected override IRepositoryCachePolicy CachePolicy + protected override IRepositoryCachePolicy CreateCachePolicy(IRuntimeCacheProvider runtimeCache) { - get - { - if (_cachePolicy != null) return _cachePolicy; - - _cachePolicy = new FullDataSetRepositoryCachePolicy(RuntimeCache, GetEntityId, /*expires:*/ false); - - return _cachePolicy; - } + return new FullDataSetRepositoryCachePolicy(runtimeCache, GetEntityId, /*expires:*/ false); } protected override IDomain PerformGet(int id) diff --git a/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs b/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs index ed9ea78ba6..9cad3200f8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs @@ -5,12 +5,8 @@ using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; -using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Rdbms; -using Umbraco.Core.Persistence.Factories; -using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories @@ -22,7 +18,7 @@ namespace Umbraco.Core.Persistence.Repositories { private readonly Guid _containerObjectType; - public EntityContainerRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, Guid containerObjectType) + public EntityContainerRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, Guid containerObjectType) : base(work, cache, logger) { var allowedContainers = new[] {Constants.ObjectTypes.DocumentTypeContainerGuid, Constants.ObjectTypes.MediaTypeContainerGuid, Constants.ObjectTypes.DataTypeContainerGuid}; @@ -31,12 +27,11 @@ namespace Umbraco.Core.Persistence.Repositories throw new InvalidOperationException("No container type exists with ID: " + _containerObjectType); } - /// - /// Do not cache anything - /// - protected override IRuntimeCacheProvider RuntimeCache + // never cache + private static readonly IRuntimeCacheProvider NullCache = new NullCacheProvider(); + protected override IRuntimeCacheProvider GetIsolatedCache(IsolatedRuntimeCache provider) { - get { return new NullCacheProvider(); } + return NullCache; } protected override EntityContainer PerformGet(int id) @@ -64,17 +59,22 @@ namespace Umbraco.Core.Persistence.Repositories protected override IEnumerable PerformGetAll(params int[] ids) { - //we need to batch these in groups of 2000 so we don't exceed the max 2100 limit - return ids.InGroupsOf(2000).SelectMany(@group => + if (ids.Any()) { - var sql = GetBaseQuery(false) - .Where("nodeObjectType=@umbracoObjectTypeId", new { umbracoObjectTypeId = NodeObjectTypeId }) - .Where(string.Format("{0} IN (@ids)", SqlSyntax.GetQuotedColumnName("id")), new { ids = @group }); + return Database.FetchByGroups(ids, 2000, batch => + GetBaseQuery(false) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .WhereIn(x => x.NodeId, batch)) + .Select(CreateEntity); + } - sql.OrderBy(x => x.Level); + // else - return Database.Fetch(sql).Select(CreateEntity); - }); + var sql = GetBaseQuery(false) + .Where("nodeObjectType=@umbracoObjectTypeId", new { umbracoObjectTypeId = NodeObjectTypeId }) + .OrderBy(x => x.Level); + + return Database.Fetch(sql).Select(CreateEntity); } protected override IEnumerable PerformGetByQuery(IQuery query) diff --git a/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs index 5d3de6fc4b..c0ee93dbb7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using NPoco; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.UnitOfWork; @@ -35,6 +37,125 @@ namespace Umbraco.Core.Persistence.Repositories public Sql Sql() => UnitOfWork.Sql(); + // fixme need to review that ... one + public IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectTypeId, long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, IQuery filter = null) + { + var isContent = objectTypeId == new Guid(Constants.ObjectTypes.Document); + var isMedia = objectTypeId == new Guid(Constants.ObjectTypes.Media); + var factory = new UmbracoEntityFactory(); + + var sqlClause = GetBaseWhere(GetBase, isContent, isMedia, sql => + { + if (filter != null) + { + foreach (var filterClaus in filter.GetWhereClauses()) + { + sql.Where(filterClaus.Item1, filterClaus.Item2); + } + } + }, objectTypeId); + var translator = new SqlTranslator(sqlClause, query); + var entitySql = translator.Translate(); + var pagedSql = entitySql.Append(GetGroupBy(isContent, isMedia, false)).OrderBy("umbracoNode.id"); + + IEnumerable result; + + if (isMedia) + { + //Treat media differently for now, as an Entity it will be returned with ALL of it's properties in the AdditionalData bag! + var pagedResult = UnitOfWork.Database.Page(pageIndex + 1, pageSize, pagedSql); + + var ids = pagedResult.Items.Select(x => (int)x.id).InGroupsOf(2000); + var entities = pagedResult.Items.Select(factory.BuildEntityFromDynamic).Cast().ToList(); + + //Now we need to merge in the property data since we need paging and we can't do this the way that the big media query was working before + foreach (var idGroup in ids) + { + var propSql = GetPropertySql(Constants.ObjectTypes.Media) + .Where("contentNodeId IN (@ids)", new { ids = idGroup }) + .OrderBy("contentNodeId"); + + //This does NOT fetch all data into memory in a list, this will read + // over the records as a data reader, this is much better for performance and memory, + // but it means that during the reading of this data set, nothing else can be read + // from SQL server otherwise we'll get an exception. + var allPropertyData = UnitOfWork.Database.Query(propSql); + + //keep track of the current property data item being enumerated + var propertyDataSetEnumerator = allPropertyData.GetEnumerator(); + var hasCurrent = false; // initially there is no enumerator.Current + + try + { + //This must be sorted by node id (which is done by SQL) because this is how we are sorting the query to lookup property types above, + // which allows us to more efficiently iterate over the large data set of property values. + foreach (var entity in entities) + { + // assemble the dtos for this def + // use the available enumerator.Current if any else move to next + while (hasCurrent || propertyDataSetEnumerator.MoveNext()) + { + if (propertyDataSetEnumerator.Current.contentNodeId == entity.Id) + { + hasCurrent = false; // enumerator.Current is not available + + //the property data goes into the additional data + entity.AdditionalData[propertyDataSetEnumerator.Current.propertyTypeAlias] = new UmbracoEntity.EntityProperty + { + PropertyEditorAlias = propertyDataSetEnumerator.Current.propertyEditorAlias, + Value = StringExtensions.IsNullOrWhiteSpace(propertyDataSetEnumerator.Current.dataNtext) + ? propertyDataSetEnumerator.Current.dataNvarchar + : StringExtensions.ConvertToJsonIfPossible(propertyDataSetEnumerator.Current.dataNtext) + }; + } + else + { + hasCurrent = true; // enumerator.Current is available for another def + break; // no more propertyDataDto for this def + } + } + } + } + finally + { + propertyDataSetEnumerator.Dispose(); + } + } + + result = entities; + } + else + { + var pagedResult = UnitOfWork.Database.Page(pageIndex + 1, pageSize, pagedSql); + result = pagedResult.Items.Select(factory.BuildEntityFromDynamic).Cast().ToList(); + } + + //The total items from the PetaPoco page query will be wrong due to the Outer join used on parent, depending on the search this will + //return duplicate results when the COUNT is used in conjuction with it, so we need to get the total on our own. + + //generate a query that does not contain the LEFT Join for parent, this would cause + //the COUNT(*) query to return the wrong + var sqlCountClause = GetBaseWhere( + (isC, isM, f) => GetBase(isC, isM, f, true), //true == is a count query + isContent, isMedia, sql => + { + if (filter != null) + { + foreach (var filterClaus in filter.GetWhereClauses()) + { + sql.Where(filterClaus.Item1, filterClaus.Item2); + } + } + }, objectTypeId); + var translatorCount = new SqlTranslator(sqlCountClause, query); + var countSql = translatorCount.Translate(); + + totalRecords = UnitOfWork.Database.ExecuteScalar(countSql); + + return result; + } + public IUmbracoEntity GetByKey(Guid key) { var sql = GetBaseWhere(GetBase, false, false, key); @@ -57,19 +178,31 @@ namespace Umbraco.Core.Persistence.Repositories if (isMedia) { - //for now treat media differently - //TODO: We should really use this methodology for Content/Members too!! since it includes properties and ALL of the dynamic db fields + //for now treat media differently and include all property data too return UnitOfWork.Database .Fetch(sql) .Transform(new UmbracoEntityRelator().MapAll) .FirstOrDefault(); } + // else + + //query = read forward data reader, do not load everything into mem + // fixme wtf is this collection thing?! + var dtos = UnitOfWork.Database.Query(sql); + var collection = new EntityDefinitionCollection(); + var factory = new UmbracoEntityFactory(); + foreach (var dto in dtos) + { + collection.AddOrUpdate(new EntityDefinition(factory, dto, isContent, false)); + } + var found = collection.FirstOrDefault(); + return found != null ? found.BuildFromDynamic() : null; + var nodeDto = UnitOfWork.Database.FirstOrDefault(sql); if (nodeDto == null) return null; - var factory = new UmbracoEntityFactory(); var entity = factory.BuildEntityFromDynamic(nodeDto); return entity; @@ -97,35 +230,38 @@ namespace Umbraco.Core.Persistence.Repositories if (isMedia) { - //for now treat media differently - //TODO: We should really use this methodology for Content/Members too!! since it includes properties and ALL of the dynamic db fields + //for now treat media differently and include all property data too return UnitOfWork.Database .Fetch(sql) .Transform(new UmbracoEntityRelator().MapAll) .FirstOrDefault(); } - var nodeDto = UnitOfWork.Database.FirstOrDefault(sql); - if (nodeDto == null) - return null; + // else + //query = read forward data reader, do not load everything into mem + var dtos = UnitOfWork.Database.Query(sql); + var collection = new EntityDefinitionCollection(); var factory = new UmbracoEntityFactory(); - var entity = factory.BuildEntityFromDynamic(nodeDto); - - return entity; + foreach (var dto in dtos) + { + collection.AddOrUpdate(new EntityDefinition(factory, dto, isContent, false)); + } + var found = collection.FirstOrDefault(); + return found != null ? found.BuildFromDynamic() : null; } public virtual IEnumerable GetAll(Guid objectTypeId, params int[] ids) { - return ids.Any() - ? PerformGetAll(objectTypeId, sql1 => sql1.Where(" umbracoNode.id in (@ids)", new { /*ids =*/ ids })) + return ids.Any() + ? PerformGetAll(objectTypeId, sql1 => sql1.Where(" umbracoNode.id in (@ids)", new { /*ids =*/ ids })) : PerformGetAll(objectTypeId); } public virtual IEnumerable GetAll(Guid objectTypeId, params Guid[] keys) { - return keys.Any() - ? PerformGetAll(objectTypeId, sql1 => sql1.Where(" umbracoNode.uniqueID in (@keys)", new { /*keys =*/ keys })) + return keys.Any() + ? PerformGetAll(objectTypeId, sql1 => sql1.Where(" umbracoNode.uniqueID in (@keys)", new { /*keys =*/ keys })) : PerformGetAll(objectTypeId); } @@ -135,19 +271,23 @@ namespace Umbraco.Core.Persistence.Repositories var isMedia = objectTypeId == new Guid(Constants.ObjectTypes.Media); var sql = GetFullSqlForEntityType(isContent, isMedia, objectTypeId, filter); - var factory = new UmbracoEntityFactory(); - if (isMedia) { - //for now treat media differently - //TODO: We should really use this methodology for Content/Members too!! since it includes properties and ALL of the dynamic db fields + //for now treat media differently and include all property data too return UnitOfWork.Database .Fetch(sql) .Transform(new UmbracoEntityRelator().MapAll); } - var dtos = UnitOfWork.Database.Fetch(sql); - return dtos.Select(dto => (UmbracoEntity) factory.BuildEntityFromDynamic(dto)); + //query = read forward data reader, do not load everything into mem + var dtos = UnitOfWork.Database.Query(sql); + var collection = new EntityDefinitionCollection(); + var factory = new UmbracoEntityFactory(); + foreach (var dto in dtos) + { + collection.AddOrUpdate(new EntityDefinition(factory, dto, isContent, false)); + } + return collection.Select(x => x.BuildFromDynamic()).ToList(); } public virtual IEnumerable GetByQuery(IQuery query) @@ -174,8 +314,6 @@ namespace Umbraco.Core.Persistence.Repositories var translator = new SqlTranslator(sqlClause, query); var entitySql = translator.Translate(); - var factory = new UmbracoEntityFactory(); - if (isMedia) { var wheres = query.GetWhereClauses().ToArray(); @@ -189,17 +327,25 @@ namespace Umbraco.Core.Persistence.Repositories } }); - //treat media differently for now - //TODO: We should really use this methodology for Content/Members too!! since it includes properties and ALL of the dynamic db fields + //for now treat media differently and include all property data too return UnitOfWork.Database .Fetch(mediaSql) .Transform(new UmbracoEntityRelator().MapAll); } - + + // else + //use dynamic so that we can get ALL properties from the SQL so we can chuck that data into our AdditionalData var finalSql = entitySql.Append(GetGroupBy(isContent, false)); - var dtos = UnitOfWork.Database.Fetch(finalSql); - return dtos.Select(factory.BuildEntityFromDynamic).Cast().ToList(); + //query = read forward data reader, do not load everything into mem + var dtos = UnitOfWork.Database.Query(finalSql); + var collection = new EntityDefinitionCollection(); + var factory = new UmbracoEntityFactory(); + foreach (var dto in dtos) + { + collection.AddOrUpdate(new EntityDefinition(factory, dto, isContent, false)); + } + return collection.Select(x => x.BuildFromDynamic()).ToList(); } public UmbracoObjectTypes GetObjectType(int id) @@ -227,7 +373,7 @@ namespace Umbraco.Core.Persistence.Repositories { var entitySql = GetBaseWhere(GetBase, isContent, isMedia, objectTypeId, key); - return isMedia + return isMedia ? GetFullSqlForMedia(entitySql.Append(GetGroupBy(isContent, true, false))) : entitySql.Append(GetGroupBy(isContent, false)); } @@ -236,7 +382,7 @@ namespace Umbraco.Core.Persistence.Repositories { var entitySql = GetBaseWhere(GetBase, isContent, isMedia, objectTypeId, id); - return isMedia + return isMedia ? GetFullSqlForMedia(entitySql.Append(GetGroupBy(isContent, true, false))) : entitySql.Append(GetGroupBy(isContent, false)); } @@ -250,11 +396,9 @@ namespace Umbraco.Core.Persistence.Repositories : entitySql.Append(GetGroupBy(isContent, false)); } - private Sql GetFullSqlForMedia(Sql entitySql, Action> filter = null) + private Sql GetPropertySql(string nodeObjectType) { - //this will add any dataNvarchar property to the output which can be added to the additional properties - - var joinSql = Sql() + var sql = Sql() .Select("contentNodeId, versionId, dataNvarchar, dataNtext, propertyEditorAlias, alias as propertyTypeAlias") .From() .InnerJoin() @@ -263,12 +407,18 @@ namespace Umbraco.Core.Persistence.Repositories .On(dto => dto.Id, dto => dto.PropertyTypeId) .InnerJoin() .On(dto => dto.DataTypeId, dto => dto.DataTypeId) - .Where("umbracoNode.nodeObjectType = @nodeObjectType", new {nodeObjectType = Constants.ObjectTypes.Media}); + .Where("umbracoNode.nodeObjectType = @nodeObjectType", new { nodeObjectType }); - if (filter != null) - { - filter(joinSql); - } + return sql; + } + + private Sql GetFullSqlForMedia(Sql entitySql, Action> filter = null) + { + //this will add any dataNvarchar property to the output which can be added to the additional properties + + var sql = GetPropertySql(Constants.ObjectTypes.Media); + + filter?.Invoke(sql); //We're going to create a query to query against the entity SQL // because we cannot group by nText columns and we have a COUNT in the entitySql we cannot simply left join @@ -278,7 +428,7 @@ namespace Umbraco.Core.Persistence.Repositories .Append("SELECT * FROM (") .Append(entitySql) .Append(") tmpTbl LEFT JOIN (") - .Append(joinSql) + .Append(sql) .Append(") as property ON id = property.contentNodeId") .OrderBy("sortOrder, id"); @@ -287,30 +437,49 @@ namespace Umbraco.Core.Persistence.Repositories protected virtual Sql GetBase(bool isContent, bool isMedia, Action> customFilter) { - var columns = new List - { - "umbracoNode.id", - "umbracoNode.trashed", - "umbracoNode.parentID", - "umbracoNode.nodeUser", - "umbracoNode.level", - "umbracoNode.path", - "umbracoNode.sortOrder", - "umbracoNode.uniqueID", - "umbracoNode.text", - "umbracoNode.nodeObjectType", - "umbracoNode.createDate", - "COUNT(parent.parentID) as children" - }; + return GetBase(isContent, isMedia, customFilter, false); + } - if (isContent || isMedia) + protected virtual Sql GetBase(bool isContent, bool isMedia, Action> customFilter, bool isCount) + { + var columns = new List(); + + if (isCount) { - columns.Add("published.versionId as publishedVersion"); - columns.Add("latest.versionId as newestVersion"); - columns.Add("contenttype.alias"); - columns.Add("contenttype.icon"); - columns.Add("contenttype.thumbnail"); - columns.Add("contenttype.isContainer"); + columns.Add("COUNT(*)"); + } + else + { + columns.AddRange(new[] + { + "umbracoNode.id", + "umbracoNode.trashed", + "umbracoNode.parentID", + "umbracoNode.nodeUser", + "umbracoNode.level", + "umbracoNode.path", + "umbracoNode.sortOrder", + "umbracoNode.uniqueID", + "umbracoNode.text", + "umbracoNode.nodeObjectType", + "umbracoNode.createDate", + "COUNT(parent.parentID) as children" + }); + + if (isContent) + { + columns.Add("published.versionId as publishedVersion"); + columns.Add("document.versionId as newestVersion"); + columns.Add("contentversion.id as versionId"); + } + + if (isContent || isMedia) + { + columns.Add("contenttype.alias"); + columns.Add("contenttype.icon"); + columns.Add("contenttype.thumbnail"); + columns.Add("contenttype.isContainer"); + } } //Creates an SQL query to return a single row for the entity @@ -321,86 +490,106 @@ namespace Umbraco.Core.Persistence.Repositories if (isContent || isMedia) { - entitySql.InnerJoin("cmsContent content").On("content.nodeId = umbracoNode.id") - .LeftJoin("cmsContentType contenttype").On("contenttype.nodeId = content.contentType") - .LeftJoin( - "(SELECT nodeId, versionId FROM cmsDocument WHERE published = 1 GROUP BY nodeId, versionId) as published") - .On("umbracoNode.id = published.nodeId") - .LeftJoin( - "(SELECT nodeId, versionId FROM cmsDocument WHERE newest = 1 GROUP BY nodeId, versionId) as latest") - .On("umbracoNode.id = latest.nodeId"); + entitySql + .InnerJoin("cmsContent content").On("content.nodeId = umbracoNode.id"); } - entitySql.LeftJoin("umbracoNode parent").On("parent.parentID = umbracoNode.id"); - - if (customFilter != null) + if (isContent) { - customFilter(entitySql); + entitySql + .InnerJoin("cmsDocument document").On("document.nodeId = umbracoNode.id") + .InnerJoin("cmsContentVersion contentversion").On("contentversion.VersionId = document.versionId") + .LeftJoin("(SELECT nodeId, versionId FROM cmsDocument WHERE published = 1 GROUP BY nodeId, versionId) as published").On("umbracoNode.id = published.nodeId"); + //.LeftJoin("(SELECT nodeId, versionId FROM cmsDocument WHERE newest = 1 GROUP BY nodeId, versionId) as latest").On("umbracoNode.id = latest.nodeId"); } + if (isContent || isMedia) + { + entitySql + .LeftJoin("cmsContentType contenttype").On("contenttype.nodeId = content.contentType"); + } + + if (isCount == false) + entitySql.LeftJoin("umbracoNode parent").On("parent.parentID = umbracoNode.id"); + + customFilter?.Invoke(entitySql); + return entitySql; } protected virtual Sql GetBaseWhere(Func>, Sql> baseQuery, bool isContent, bool isMedia, Action> filter, Guid nodeObjectType) { var sql = baseQuery(isContent, isMedia, filter) - .Where("umbracoNode.nodeObjectType = @NodeObjectType", new { NodeObjectType = nodeObjectType }); + .Where("umbracoNode.nodeObjectType = @nodeObjectType", new { nodeObjectType }); + if (isContent) + sql.Where("document.newest = 1"); return sql; } protected virtual Sql GetBaseWhere(Func>, Sql> baseQuery, bool isContent, bool isMedia, int id) { var sql = baseQuery(isContent, isMedia, null) - .Where("umbracoNode.id = @Id", new { Id = id }) - .Append(GetGroupBy(isContent, isMedia)); + .Where("umbracoNode.id = @id", new { id }); + if (isContent) + sql.Where("document.newest = 1"); + sql.Append(GetGroupBy(isContent, isMedia)); return sql; } protected virtual Sql GetBaseWhere(Func>, Sql> baseQuery, bool isContent, bool isMedia, Guid key) { var sql = baseQuery(isContent, isMedia, null) - .Where("umbracoNode.uniqueID = @UniqueID", new { UniqueID = key }) - .Append(GetGroupBy(isContent, isMedia)); + .Where("umbracoNode.uniqueID = @uniqueID", new { uniqueID = key }); + if (isContent) + sql.Where("document.newest = 1"); + sql.Append(GetGroupBy(isContent, isMedia)); return sql; } protected virtual Sql GetBaseWhere(Func>, Sql> baseQuery, bool isContent, bool isMedia, Guid nodeObjectType, int id) { var sql = baseQuery(isContent, isMedia, null) - .Where("umbracoNode.id = @Id AND umbracoNode.nodeObjectType = @NodeObjectType", - new {Id = id, NodeObjectType = nodeObjectType}); + .Where("umbracoNode.id = @id AND umbracoNode.nodeObjectType = @nodeObjectType", new { id, nodeObjectType}); + if (isContent) + sql.Where("document.newest = 1"); return sql; } protected virtual Sql GetBaseWhere(Func>, Sql> baseQuery, bool isContent, bool isMedia, Guid nodeObjectType, Guid key) { var sql = baseQuery(isContent, isMedia, null) - .Where("umbracoNode.uniqueID = @UniqueID AND umbracoNode.nodeObjectType = @NodeObjectType", - new { UniqueID = key, NodeObjectType = nodeObjectType }); + .Where("umbracoNode.uniqueID = @uniqueID AND umbracoNode.nodeObjectType = @nodeObjectType", new { uniqueID = key, nodeObjectType }); + if (isContent) + sql.Where("document.newest = 1"); return sql; } protected virtual Sql GetGroupBy(bool isContent, bool isMedia, bool includeSort = true) { var columns = new List - { - "umbracoNode.id", - "umbracoNode.trashed", - "umbracoNode.parentID", - "umbracoNode.nodeUser", - "umbracoNode.level", - "umbracoNode.path", - "umbracoNode.sortOrder", - "umbracoNode.uniqueID", - "umbracoNode.text", - "umbracoNode.nodeObjectType", - "umbracoNode.createDate" - }; + { + "umbracoNode.id", + "umbracoNode.trashed", + "umbracoNode.parentID", + "umbracoNode.nodeUser", + "umbracoNode.level", + "umbracoNode.path", + "umbracoNode.sortOrder", + "umbracoNode.uniqueID", + "umbracoNode.text", + "umbracoNode.nodeObjectType", + "umbracoNode.createDate" + }; + + if (isContent) + { + columns.Add("published.versionId"); + columns.Add("document.versionId"); + columns.Add("contentVersion.id"); + } if (isContent || isMedia) { - columns.Add("published.versionId"); - columns.Add("latest.versionId"); columns.Add("contenttype.alias"); columns.Add("contenttype.icon"); columns.Add("contenttype.thumbnail"); @@ -430,34 +619,20 @@ namespace Umbraco.Core.Persistence.Repositories UnitOfWork.DisposeIfDisposable(); } - #region umbracoNode POCO - Extends NodeDto - [TableName("umbracoNode")] - [PrimaryKey("id")] - [ExplicitColumns] - internal class UmbracoEntityDto : NodeDto + public bool Exists(Guid key) { - [Column("children")] - public int Children { get; set; } - - [Column("publishedVersion")] - public Guid PublishedVersion { get; set; } - - [Column("newestVersion")] - public Guid NewestVersion { get; set; } - - [Column("alias")] - public string Alias { get; set; } - - [Column("icon")] - public string Icon { get; set; } - - [Column("thumbnail")] - public string Thumbnail { get; set; } - - [ResultColumn] - public List UmbracoPropertyDtos { get; set; } + var sql = new Sql().Select("COUNT(*)").From("umbracoNode").Where("uniqueID=@uniqueID", new { uniqueID = key }); + return UnitOfWork.Database.ExecuteScalar(sql) > 0; } + public bool Exists(int id) + { + var sql = new Sql().Select("COUNT(*)").From("umbracoNode").Where("id=@id", new { id }); + return UnitOfWork.Database.ExecuteScalar(sql) > 0; + } + + #region Classes + [ExplicitColumns] internal class UmbracoPropertyDto { @@ -561,6 +736,98 @@ namespace Umbraco.Core.Persistence.Repositories return prev; } } + + // fixme need to review what's below + // comes from 7.6, similar to what's in VersionableRepositoryBase + // not sure it really makes sense... + + private class EntityDefinitionCollection : KeyedCollection + { + protected override int GetKeyForItem(EntityDefinition item) + { + return item.Id; + } + + /// + /// if this key already exists if it does then we need to check + /// if the existing item is 'older' than the new item and if that is the case we'll replace the older one + /// + /// + /// + public bool AddOrUpdate(EntityDefinition item) + { + if (Dictionary == null) + { + Add(item); + return true; + } + + var key = GetKeyForItem(item); + if (TryGetValue(key, out EntityDefinition found)) + { + //it already exists and it's older so we need to replace it + if (item.VersionId > found.VersionId) + { + var currIndex = Items.IndexOf(found); + if (currIndex == -1) + throw new IndexOutOfRangeException("Could not find the item in the list: " + found.Id); + + //replace the current one with the newer one + SetItem(currIndex, item); + return true; + } + //could not add or update + return false; + } + + Add(item); + return true; + } + + private bool TryGetValue(int key, out EntityDefinition val) + { + if (Dictionary != null) return Dictionary.TryGetValue(key, out val); + + val = null; + return false; + } + } + + private class EntityDefinition + { + private readonly UmbracoEntityFactory _factory; + private readonly dynamic _entity; + private readonly bool _isContent; + private readonly bool _isMedia; + + public EntityDefinition(UmbracoEntityFactory factory, dynamic entity, bool isContent, bool isMedia) + { + _factory = factory; + _entity = entity; + _isContent = isContent; + _isMedia = isMedia; + } + + public IUmbracoEntity BuildFromDynamic() + { + return _factory.BuildEntityFromDynamic(_entity); + } + + public int Id => _entity.id; + + public int VersionId + { + get + { + if (_isContent || _isMedia) + { + return _entity.versionId; + } + return _entity.id; + } + } + } + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs index 2c48397792..72acb80e1b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs @@ -16,7 +16,7 @@ namespace Umbraco.Core.Persistence.Repositories { internal class ExternalLoginRepository : NPocoRepositoryBase, IExternalLoginRepository { - public ExternalLoginRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger) + public ExternalLoginRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger) : base(work, cache, logger) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/FileRepository.cs b/src/Umbraco.Core/Persistence/Repositories/FileRepository.cs index e4573d85ec..5b3c2cf75f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/FileRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/FileRepository.cs @@ -12,36 +12,27 @@ namespace Umbraco.Core.Persistence.Repositories internal abstract class FileRepository : DisposableObject, IUnitOfWorkRepository, IRepository where TEntity : IFile { - private IUnitOfWork _work; - private readonly IFileSystem _fileSystem; - protected FileRepository(IUnitOfWork work, IFileSystem fileSystem) { - _work = work; - _fileSystem = fileSystem; + UnitOfWork = work; + FileSystem = fileSystem; } /// /// Returns the Unit of Work added to the repository /// - protected IUnitOfWork UnitOfWork - { - get { return _work; } - } + protected IUnitOfWork UnitOfWork { get; } - protected IFileSystem FileSystem - { - get { return _fileSystem; } - } + protected IFileSystem FileSystem { get; } public virtual void AddFolder(string folderPath) { - _work.RegisterCreated(new Folder(folderPath), this); + UnitOfWork.RegisterCreated(new Folder(folderPath), this); } public virtual void DeleteFolder(string folderPath) { - _work.RegisterDeleted(new Folder(folderPath), this); + UnitOfWork.RegisterDeleted(new Folder(folderPath), this); } #region Implementation of IRepository @@ -50,19 +41,19 @@ namespace Umbraco.Core.Persistence.Repositories { if (FileSystem.FileExists(entity.OriginalPath) == false) { - _work.RegisterCreated(entity, this); + UnitOfWork.RegisterCreated(entity, this); } else { - _work.RegisterUpdated(entity, this); + UnitOfWork.RegisterUpdated(entity, this); } } public virtual void Delete(TEntity entity) { - if (_work != null) + if (UnitOfWork != null) { - _work.RegisterDeleted(entity, this); + UnitOfWork.RegisterDeleted(entity, this); } } @@ -72,12 +63,7 @@ namespace Umbraco.Core.Persistence.Repositories public virtual bool Exists(TId id) { - return _fileSystem.FileExists(id.ToString()); - } - - public void SetUnitOfWork(IUnitOfWork work) - { - _work = work; + return FileSystem.FileExists(id.ToString()); } #endregion @@ -121,12 +107,12 @@ namespace Umbraco.Core.Persistence.Repositories internal virtual void PersistNewFolder(Folder entity) { - _fileSystem.CreateFolder(entity.Path); + FileSystem.CreateFolder(entity.Path); } internal virtual void PersistDeletedFolder(Folder entity) { - _fileSystem.DeleteDirectory(entity.Path); + FileSystem.DeleteDirectory(entity.Path); } #region Abstract IUnitOfWorkRepository Methods @@ -170,9 +156,9 @@ namespace Umbraco.Core.Persistence.Repositories protected virtual void PersistDeletedItem(TEntity entity) { - if (_fileSystem.FileExists(entity.Path)) + if (FileSystem.FileExists(entity.Path)) { - _fileSystem.DeleteFile(entity.Path); + FileSystem.DeleteFile(entity.Path); } } @@ -234,15 +220,31 @@ namespace Umbraco.Core.Persistence.Repositories } } - /// - /// Dispose any disposable properties - /// - /// - /// Dispose the unit of work - /// - protected override void DisposeResources() + public long GetFileSize(string filename) + { + if (FileSystem.FileExists(filename) == false) + return -1; + + try + { + return FileSystem.GetSize(filename); + } + catch + { + return -1; // deal with race conds + } + } + + /// + /// Dispose any disposable properties + /// + /// + /// Dispose the unit of work + /// + protected override void DisposeResources() { - _work.DisposeIfDisposable(); + // fixme - wtf in v8? + UnitOfWork.DisposeIfDisposable(); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IAuditRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IAuditRepository.cs index 68193119e5..376895e8e7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IAuditRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IAuditRepository.cs @@ -1,9 +1,9 @@ -using System.Collections; using Umbraco.Core.Models; +using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { - public interface IAuditRepository : IQueryRepository + public interface IAuditRepository : IUnitOfWorkRepository, IQueryRepository { void CleanLogs(int maximumAgeOfLogsInMinutes); } diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs index 11a98c678e..1e4ebb34ca 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Xml.Linq; +using System.Collections.Generic; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories @@ -55,21 +52,5 @@ namespace Umbraco.Core.Persistence.Repositories /// /// IEnumerable GetPermissionsForEntity(int entityId); - - /// - /// Gets paged content results - /// - /// Query to excute - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field - /// - /// A value indicating whether to get the 'newest' or all of them. - /// An Enumerable list of objects - IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter = null, bool newest = true); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs index c0c23ee0ed..d9d026cd52 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Umbraco.Core.Events; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Querying; @@ -30,13 +29,5 @@ namespace Umbraco.Core.Persistence.Repositories /// /// IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes); - - /// - /// Derives a unique alias from an existing alias. - /// - /// The original alias. - /// The original alias with a number appended to it, so that it is unique. - /// /// Unique accross all content, media and member types. - string GetUniqueAlias(string alias); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepositoryBase.cs index c7a9f35200..fbdcd67853 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepositoryBase.cs @@ -19,5 +19,13 @@ namespace Umbraco.Core.Persistence.Repositories /// The original alias with a number appended to it, so that it is unique. /// Unique accross all content, media and member types. string GetUniqueAlias(string alias); + + + /// + /// Gets a value indicating whether there is a list view content item in the path. + /// + /// + /// + bool HasContainerInPath(string contentPath); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IEntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IEntityRepository.cs index ba42548906..0f8321bd42 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IEntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IEntityRepository.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories @@ -21,5 +22,36 @@ namespace Umbraco.Core.Persistence.Repositories UmbracoObjectTypes GetObjectType(int id); UmbracoObjectTypes GetObjectType(Guid key); + + + /// + /// Gets paged results + /// + /// Query to excute + /// + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// + /// An Enumerable list of objects + IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectTypeId, long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, IQuery filter = null); + + /// + /// Returns true if the entity exists + /// + /// + /// + bool Exists(Guid key); + + /// + /// Returns true if the entity exists + /// + /// + /// + bool Exists(int id); + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMacroRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMacroRepository.cs index 4bcaf21846..5f33c3fa47 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMacroRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMacroRepository.cs @@ -1,9 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core.Models; namespace Umbraco.Core.Persistence.Repositories { - internal interface IMacroRepository : IQueryRepository + internal interface IMacroRepository : IQueryRepository, IReadRepository { //IEnumerable GetAll(params string[] aliases); diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs index a47b535e97..70ac7bd0ba 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs @@ -1,29 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Xml.Linq; -using Umbraco.Core.Models; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; -using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Models; namespace Umbraco.Core.Persistence.Repositories { public interface IMediaRepository : IRepositoryVersionable, IRecycleBinRepository { IMedia GetMediaByPath(string mediaPath); - - /// - /// Gets paged media results - /// - /// Query to excute - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field - /// - /// An Enumerable list of objects - IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter = null); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberGroupRepository.cs index 934b10af7f..764dd00bbf 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberGroupRepository.cs @@ -1,9 +1,10 @@ using System.Collections.Generic; using Umbraco.Core.Models; +using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { - public interface IMemberGroupRepository : IQueryRepository + public interface IMemberGroupRepository : IUnitOfWorkRepository, IQueryRepository { /// /// Gets a member group by it's name diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs index 2e03c339d7..bead7ff1bc 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs @@ -1,15 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using System.Xml.Linq; +using System.Collections.Generic; using Umbraco.Core.Models; -using Umbraco.Core.Models.Rdbms; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { - public interface IMemberRepository : IRepositoryVersionable + public interface IMemberRepository : IUnitOfWorkRepository, IRepositoryVersionable { /// /// Finds members in a given role @@ -40,24 +36,5 @@ namespace Umbraco.Core.Persistence.Repositories /// /// int GetCountByQuery(IQuery query); - - /// - /// Gets paged member results - /// - /// The query. - /// Index of the page. - /// Size of the page. - /// The total records. - /// The order by column - /// The order direction. - /// Flag to indicate when ordering by system field - /// Search query - /// - IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, string filter = ""); - - //IEnumerable GetPagedResultsByQuery( - // Sql sql, int pageIndex, int pageSize, out int totalRecords, - // Func, int[]> resolveIds); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberTypeRepository.cs index 9d55df0d02..5cf4e52b88 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberTypeRepository.cs @@ -1,7 +1,8 @@ using Umbraco.Core.Models; +using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { - public interface IMemberTypeRepository : IContentTypeRepositoryBase + public interface IMemberTypeRepository : IUnitOfWorkRepository, IContentTypeRepositoryBase { } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMigrationEntryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMigrationEntryRepository.cs index 4258a74f45..20809a8e74 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMigrationEntryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMigrationEntryRepository.cs @@ -1,10 +1,10 @@ -using System; -using Semver; +using Semver; using Umbraco.Core.Models; +using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { - public interface IMigrationEntryRepository : IQueryRepository + public interface IMigrationEntryRepository : IUnitOfWorkRepository, IQueryRepository { IMigrationEntry FindEntry(string migrationName, SemVersion version); } diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IPartialViewRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IPartialViewRepository.cs index 7c8391a77a..27342fe643 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IPartialViewRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IPartialViewRepository.cs @@ -10,5 +10,6 @@ namespace Umbraco.Core.Persistence.Repositories bool ValidatePartialView(IPartialView partialView); Stream GetFileContentStream(string filepath); void SetFileContent(string filepath, Stream content); + long GetFileSize(string filepath); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IPublicAccessRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IPublicAccessRepository.cs index f8251103a5..1e71273db3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IPublicAccessRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IPublicAccessRepository.cs @@ -1,11 +1,9 @@ using System; -using System.Collections; -using System.Collections.Generic; using Umbraco.Core.Models; +using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { - public interface IPublicAccessRepository : IQueryRepository - { - } + public interface IPublicAccessRepository : IUnitOfWorkRepository, IQueryRepository + { } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRedirectUrlRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRedirectUrlRepository.cs index 98d7dd0179..77b9fc3531 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRedirectUrlRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRedirectUrlRepository.cs @@ -1,13 +1,14 @@ using System; using System.Collections.Generic; using Umbraco.Core.Models; +using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { /// /// Defines the repository. /// - public interface IRedirectUrlRepository : IQueryRepository + public interface IRedirectUrlRepository : IUnitOfWorkRepository, IQueryRepository { /// /// Gets a redirect url. diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRelationRepository.cs index 52ebbdc41d..5a296df9f0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRelationRepository.cs @@ -1,8 +1,9 @@ using Umbraco.Core.Models; +using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { - public interface IRelationRepository : IQueryRepository + public interface IRelationRepository : IUnitOfWorkRepository, IQueryRepository { } diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRelationTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRelationTypeRepository.cs index a8ecc29d32..d57dca66d9 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRelationTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRelationTypeRepository.cs @@ -1,9 +1,9 @@ -using Umbraco.Core.Models; +using System; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { - public interface IRelationTypeRepository : IQueryRepository - { - - } + public interface IRelationTypeRepository : IUnitOfWorkRepository, IQueryRepository, IReadRepository + { } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs index ff27afa467..27a441d71f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; using System.Xml.Linq; +using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { @@ -58,5 +61,20 @@ namespace Umbraco.Core.Persistence.Repositories /// Id of the object to delete versions from /// Latest version date void DeleteVersions(int id, DateTime versionDate); + + /// + /// Gets paged content results + /// + /// Query to excute + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// + /// An Enumerable list of objects + IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter = null, bool newest = true); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IScriptRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IScriptRepository.cs index eacb818faa..c2c0a0ae84 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IScriptRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IScriptRepository.cs @@ -8,5 +8,6 @@ namespace Umbraco.Core.Persistence.Repositories bool ValidateScript(Script script); Stream GetFileContentStream(string filepath); void SetFileContent(string filepath, Stream content); + long GetFileSize(string filepath); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IServerRegistrationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IServerRegistrationRepository.cs index 6bd4e54944..bc3bd872f9 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IServerRegistrationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IServerRegistrationRepository.cs @@ -1,9 +1,10 @@ using System; using Umbraco.Core.Models; +using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { - public interface IServerRegistrationRepository : IQueryRepository + public interface IServerRegistrationRepository : IUnitOfWorkRepository, IQueryRepository { void DeactiveStaleServers(TimeSpan staleTimeout); } diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IStylesheetRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IStylesheetRepository.cs index 323f11d339..5fadf7b01c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IStylesheetRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IStylesheetRepository.cs @@ -8,5 +8,6 @@ namespace Umbraco.Core.Persistence.Repositories bool ValidateStylesheet(Stylesheet stylesheet); Stream GetFileContentStream(string filepath); void SetFileContent(string filepath, Stream content); + long GetFileSize(string filepath); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITagRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITagRepository.cs index 46f25924b6..d705e4551f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITagRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITagRepository.cs @@ -1,10 +1,11 @@ using System; using System.Collections.Generic; using Umbraco.Core.Models; +using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { - public interface ITagRepository : IQueryRepository + public interface ITagRepository : IUnitOfWorkRepository, IQueryRepository { TaggedEntity GetTaggedEntityByKey(Guid key); diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITaskRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITaskRepository.cs index e9b5b0f267..f0df763fcd 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITaskRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITaskRepository.cs @@ -1,10 +1,10 @@ -using System; using System.Collections.Generic; using Umbraco.Core.Models; +using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { - public interface ITaskRepository : IQueryRepository + public interface ITaskRepository : IUnitOfWorkRepository, IQueryRepository { IEnumerable GetTasks(int? itemId = null, int? assignedUser = null, int? ownerUser = null, string taskTypeAlias = null, bool includeClosed = false); } diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITaskTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITaskTypeRepository.cs index f023a7baf6..34a6379f3a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITaskTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITaskTypeRepository.cs @@ -1,9 +1,8 @@ using Umbraco.Core.Models; +using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { - public interface ITaskTypeRepository : IQueryRepository - { - - } + public interface ITaskTypeRepository : IUnitOfWorkRepository, IQueryRepository + { } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITemplateRepository.cs index ad30fbf9ee..4f1b64fc87 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITemplateRepository.cs @@ -38,16 +38,16 @@ namespace Umbraco.Core.Persistence.Repositories TemplateNode FindTemplateInTree(TemplateNode anyNode, string alias); /// - /// This checks what the default rendering engine is set in config but then also ensures that there isn't already - /// a template that exists in the opposite rendering engine's template folder, then returns the appropriate + /// This checks what the default rendering engine is set in config but then also ensures that there isn't already + /// a template that exists in the opposite rendering engine's template folder, then returns the appropriate /// rendering engine to use. - /// + /// /// /// /// The reason this is required is because for example, if you have a master page file already existing under ~/masterpages/Blah.aspx - /// and then you go to create a template in the tree called Blah and the default rendering engine is MVC, it will create a Blah.cshtml - /// empty template in ~/Views. This means every page that is using Blah will go to MVC and render an empty page. - /// This is mostly related to installing packages since packages install file templates to the file system and then create the + /// and then you go to create a template in the tree called Blah and the default rendering engine is MVC, it will create a Blah.cshtml + /// empty template in ~/Views. This means every page that is using Blah will go to MVC and render an empty page. + /// This is mostly related to installing packages since packages install file templates to the file system and then create the /// templates in business logic. Without this, it could cause the wrong rendering engine to be used for a package. /// RenderingEngine DetermineTemplateRenderingEngine(ITemplate template); @@ -72,5 +72,7 @@ namespace Umbraco.Core.Persistence.Repositories /// The filesystem path to the template. /// The content of the template. void SetFileContent(string filepath, Stream content); + + long GetFileSize(string filepath); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserRepository.cs index a41d6bbfe9..d6fd53f2ad 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserRepository.cs @@ -3,10 +3,11 @@ using System.Collections.Generic; using System.Linq.Expressions; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { - public interface IUserRepository : IQueryRepository + public interface IUserRepository : IUnitOfWorkRepository, IQueryRepository { /// /// Gets the count of items based on a complex query diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserTypeRepository.cs index ad26593803..5507b56472 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserTypeRepository.cs @@ -1,8 +1,9 @@ using Umbraco.Core.Models.Membership; +using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { - public interface IUserTypeRepository : IQueryRepository + public interface IUserTypeRepository : IUnitOfWorkRepository, IQueryRepository { } diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IXsltFileRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IXsltFileRepository.cs index 8bff985400..4c56b6eb18 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IXsltFileRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IXsltFileRepository.cs @@ -8,5 +8,6 @@ namespace Umbraco.Core.Persistence.Repositories bool ValidateXsltFile(XsltFile xsltFile); Stream GetFileContentStream(string filepath); void SetFileContent(string filepath, Stream content); + long GetFileSize(string filepath); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs index bd1630589b..4a0d96e049 100644 --- a/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs @@ -21,21 +21,13 @@ namespace Umbraco.Core.Persistence.Repositories { private IRepositoryCachePolicy _cachePolicy; - public LanguageRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger) + public LanguageRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger) : base(work, cache, logger) + { } + + protected override IRepositoryCachePolicy CreateCachePolicy(IRuntimeCacheProvider runtimeCache) { - } - - protected override IRepositoryCachePolicy CachePolicy - { - get - { - if (_cachePolicy != null) return _cachePolicy; - - _cachePolicy = new FullDataSetRepositoryCachePolicy(RuntimeCache, GetEntityId, /*expires:*/ false); - - return _cachePolicy; - } + return new FullDataSetRepositoryCachePolicy(runtimeCache, GetEntityId, /*expires:*/ false); } #region Overrides of RepositoryBase @@ -139,8 +131,8 @@ namespace Umbraco.Core.Persistence.Repositories entity.ResetDirtyProperties(); //Clear the cache entries that exist by key/iso - RuntimeCache.ClearCacheItem(GetCacheIdKey(entity.IsoCode)); - RuntimeCache.ClearCacheItem(GetCacheIdKey(entity.CultureName)); + IsolatedCache.ClearCacheItem(GetCacheIdKey(entity.IsoCode)); + IsolatedCache.ClearCacheItem(GetCacheIdKey(entity.CultureName)); } protected override void PersistDeletedItem(ILanguage entity) @@ -148,8 +140,8 @@ namespace Umbraco.Core.Persistence.Repositories base.PersistDeletedItem(entity); //Clear the cache entries that exist by key/iso - RuntimeCache.ClearCacheItem(GetCacheIdKey(entity.IsoCode)); - RuntimeCache.ClearCacheItem(GetCacheIdKey(entity.CultureName)); + IsolatedCache.ClearCacheItem(GetCacheIdKey(entity.IsoCode)); + IsolatedCache.ClearCacheItem(GetCacheIdKey(entity.CultureName)); } #endregion @@ -172,7 +164,5 @@ namespace Umbraco.Core.Persistence.Repositories //use the underlying GetAll which will force cache all languages return GetAll().FirstOrDefault(x => x.IsoCode.InvariantEquals(isoCode)); } - - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/MacroRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MacroRepository.cs index 69818589e1..2b1ee07d86 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MacroRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MacroRepository.cs @@ -16,17 +16,25 @@ namespace Umbraco.Core.Persistence.Repositories { internal class MacroRepository : NPocoRepositoryBase, IMacroRepository { - - public MacroRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger) + public MacroRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger) : base(work, cache, logger) - { - } + { } protected override IMacro PerformGet(int id) { var sql = GetBaseQuery(false); sql.Where(GetBaseWhereClause(), new { Id = id }); + return GetBySql(sql); + } + public IMacro Get(Guid id) + { + var sql = GetBaseQuery().Where("uniqueId=@Id", new { Id = id }); + return GetBySql(sql); + } + + private IMacro GetBySql(Sql sql) + { var macroDto = Database .FetchOneToMany(x => x.MacroPropertyDtos, sql) .FirstOrDefault(); @@ -44,14 +52,26 @@ namespace Umbraco.Core.Persistence.Repositories return entity; } + public IEnumerable GetAll(params Guid[] ids) + { + return ids.Length > 0 ? ids.Select(Get) : GetAllNoIds(); + } + + public bool Exists(Guid id) + { + return Get(id) != null; + } + protected override IEnumerable PerformGetAll(params int[] ids) { - if (ids.Any()) - { - return PerformGetAllOnIds(ids); - } + return ids.Length > 0 ? ids.Select(Get) : GetAllNoIds(); + } - var sql = GetBaseQuery(false); + 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) @@ -59,15 +79,6 @@ namespace Umbraco.Core.Persistence.Repositories .ToArray(); // do it now and once } - private IEnumerable PerformGetAllOnIds(params int[] ids) - { - if (ids.Any() == false) yield break; - foreach (var id in ids) - { - yield return Get(id); - } - } - private IEnumerable ConvertFromDtos(IEnumerable dtos) { var factory = new MacroFactory(); @@ -121,10 +132,7 @@ namespace Umbraco.Core.Persistence.Repositories return list; } - protected override Guid NodeObjectTypeId - { - get { throw new NotImplementedException(); } - } + protected override Guid NodeObjectTypeId => throw new NotImplementedException(); protected override void PersistNewItem(IMacro entity) { @@ -160,68 +168,57 @@ namespace Umbraco.Core.Persistence.Repositories var macro = (Macro)entity; if (macro.IsPropertyDirty("Properties") || macro.Properties.Any(x => x.IsDirty())) { - //now we need to delete any props that have been removed - foreach (var propAlias in macro.RemovedProperties) - { - //delete the property - Database.Delete("WHERE macro=@macroId AND macroPropertyAlias=@propAlias", - new { macroId = macro.Id, propAlias = propAlias }); - } + 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 }); - //for any that exist on the object, we need to determine if we need to update or insert + // detect new aliases, replace with temp aliases + // this ensures that we don't have collisions, ever + var aliases = new Dictionary(); foreach (var propDto in dto.MacroPropertyDtos) { - if (macro.AddedProperties.Contains(propDto.Alias)) + var prop = macro.Properties.FirstOrDefault(x => x.Id == propDto.Id); + if (prop == null) throw new Exception("oops: property."); + if (propDto.Id == 0 || prop.IsPropertyDirty("Alias")) { - //we need to insert since this was added and re-assign the new id - var propId = Convert.ToInt32(Database.Insert(propDto)); - macro.Properties[propDto.Alias].Id = propId; + 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 { - //This will only work if the Alias hasn't been changed - if (macro.Properties.ContainsKey(propDto.Alias)) - { - //only update if it's dirty - if (macro.Properties[propDto.Alias].IsDirty()) - { - Database.Update(propDto); - } - } - else - { - var property = macro.Properties.FirstOrDefault(x => x.Id == propDto.Id); - if (property != null && property.IsDirty()) - { - Database.Update(propDto); - } - } + // update + var property = macro.Properties.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(); } - - //public IEnumerable GetAll(params string[] aliases) - //{ - // if (aliases.Any()) - // { - // var q = new Query(); - // foreach (var alias in aliases) - // { - // q.Where(macro => macro.Alias == alias); - // } - - // var wheres = string.Join(" OR ", q.WhereClauses()); - // } - // else - // { - // return GetAll(new int[] {}); - // } - - //} } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index 0d1038455d..d7ffc1ec38 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -25,24 +25,15 @@ namespace Umbraco.Core.Persistence.Repositories private readonly IMediaTypeRepository _mediaTypeRepository; private readonly ITagRepository _tagRepository; - public MediaRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, IContentSection contentSection) - : base(work, cache, logger, contentSection) + public MediaRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, IContentSection contentSection) + : base(work, cache, logger /*, contentSection*/) { - if (mediaTypeRepository == null) throw new ArgumentNullException(nameof(mediaTypeRepository)); - if (tagRepository == null) throw new ArgumentNullException(nameof(tagRepository)); - _mediaTypeRepository = mediaTypeRepository; - _tagRepository = tagRepository; + _mediaTypeRepository = mediaTypeRepository ?? throw new ArgumentNullException(nameof(mediaTypeRepository)); + _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); EnsureUniqueNaming = contentSection.EnsureUniqueNaming; } - public void SetNoCachePolicy() - { - // using NoCache here means that we are NOT updating the cache - // so this should be OK for reads but NOT for writes! - CachePolicy = new NoCacheRepositoryCachePolicy(); - } - - protected override MediaRepository Instance => this; + protected override MediaRepository This => this; public bool EnsureUniqueNaming { get; } @@ -90,14 +81,29 @@ namespace Umbraco.Core.Persistence.Repositories #region Overrides of NPocoRepositoryBase protected override Sql GetBaseQuery(bool isCount) + { + return GetBaseQuery(isCount ? QueryType.Count : QueryType.Single); + } + + protected override Sql GetBaseQuery(QueryType queryType) { var sql = Sql(); - sql = isCount - ? sql.SelectCount() - : sql.Select(r => - r.Select(rr => - rr.Select())); + switch (queryType) + { + case QueryType.Count: + sql = sql.SelectCount(); + break; + case QueryType.Ids: + sql = sql.Select("cmsContentVersion.contentId"); + break; + case QueryType.Many: + case QueryType.Single: + sql = sql.Select(r => + r.Select(rr => + rr.Select())); + break; + } sql .From() @@ -160,19 +166,7 @@ namespace Umbraco.Core.Persistence.Repositories if (dto == null) return null; - var mediaType = _mediaTypeRepository.Get(dto.ContentDto.ContentTypeId); - - var factory = new MediaFactory(mediaType, NodeObjectTypeId, dto.NodeId); - var media = factory.BuildEntity(dto); - - var properties = GetPropertyCollection(new[] { new DocumentDefinition(dto.NodeId, dto.VersionId, media.UpdateDate, media.CreateDate, mediaType) }); - - media.Properties = properties[dto.NodeId]; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)media).ResetDirtyProperties(false); - return media; + return CreateMediaFromDto(dto, versionId); } public IMedia GetMediaByPath(string mediaPath) @@ -254,10 +248,11 @@ namespace Umbraco.Core.Persistence.Repositories 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); + var unused = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); //Update with new correct path nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + nodeDto.ValidatePathWithException(); Database.Update(nodeDto); //Update entity with correct values @@ -333,7 +328,8 @@ namespace Umbraco.Core.Persistence.Repositories //Updates the (base) node data - umbracoNode var nodeDto = dto.ContentDto.NodeDto; - var o = Database.Update(nodeDto); + nodeDto.ValidatePathWithException(); + var unused = Database.Update(nodeDto); //Only update this DTO if the contentType has actually changed if (contentDto.ContentTypeId != entity.ContentTypeId) @@ -389,7 +385,7 @@ namespace Umbraco.Core.Persistence.Repositories protected override void PersistDeletedItem(IMedia entity) { // raise event first else potential FK issues - OnUowRemovingEntity(new UnitOfWorkEntityEventArgs(this.UnitOfWork, entity)); + OnUowRemovingEntity(new UnitOfWorkEntityEventArgs(UnitOfWork, entity)); base.PersistDeletedItem(entity); } @@ -414,14 +410,15 @@ namespace Umbraco.Core.Persistence.Repositories /// /// An Enumerable list of objects public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter = null) + string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter = null, bool newest = true) { Sql filterSql = null; if (filter != null) { - foreach (var filterClaus in filter.GetWhereClauses()) + filterSql = Sql(); + foreach (var clause in filter.GetWhereClauses()) { - filterSql = Sql().Append($"AND ({filterClaus.Item1})", filterClaus.Item2); + filterSql = filterSql.Append($"AND ({clause.Item1})", clause.Item2); } } @@ -433,16 +430,17 @@ namespace Umbraco.Core.Persistence.Repositories private IEnumerable MapQueryDtos(List dtos, bool withCache = false) { var content = new IMedia[dtos.Count]; - var defs = new List(); + var temps = new List(); + var contentTypes = new Dictionary(); for (var i = 0; i < dtos.Count; i++) { var dto = dtos[i]; - // if the cache contains the item, use it if (withCache) { - var cached = RuntimeCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); + // if the cache contains the (proper version of the) item, use it + var cached = IsolatedCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); if (cached != null) { content[i] = cached; @@ -451,65 +449,41 @@ namespace Umbraco.Core.Persistence.Repositories } // else, need to fetch from the database - // content type repository is full-cache so OK to get each one independently - var contentType = _mediaTypeRepository.Get(dto.ContentDto.ContentTypeId); - var factory = new MediaFactory(contentType, NodeObjectTypeId, dto.NodeId); - content[i] = factory.BuildEntity(dto); + + // 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 + if (contentTypes.TryGetValue(dto.ContentDto.ContentTypeId, out IMediaType contentType) == false) + contentTypes[dto.ContentDto.ContentTypeId] = contentType = _mediaTypeRepository.Get(dto.ContentDto.ContentTypeId); + + var c = content[i] = MediaFactory.BuildEntity(dto, contentType); // need properties - defs.Add(new DocumentDefinition( + temps.Add(new TempContent( dto.NodeId, dto.VersionId, dto.VersionDate, dto.ContentDto.NodeDto.CreateDate, - contentType + contentType, + c )); } // load all properties for all documents from database in 1 query - var propertyData = GetPropertyCollection(defs.ToArray()); + var propertyData = GetPropertyCollection(temps); // assign - var dtoIndex = 0; - foreach (var def in defs) + foreach (var temp in temps) { - // move to corresponding item (which has to exist) - while (dtos[dtoIndex].NodeId != def.Id) dtoIndex++; - - // complete the item - var cc = content[dtoIndex]; - cc.Properties = propertyData[cc.Id]; + temp.Content.Properties = propertyData[temp.Version]; //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 - ((Entity)cc).ResetDirtyProperties(false); + ((Entity) temp.Content).ResetDirtyProperties(false); } return content; } - /// - /// Private method to create a media object from a ContentDto - /// - /// - /// - /// - /// - private IMedia CreateMediaFromDto(ContentVersionDto dto, - IMediaType contentType, - PropertyCollection propCollection) - { - var factory = new MediaFactory(contentType, NodeObjectTypeId, dto.NodeId); - var media = factory.BuildEntity(dto); - - media.Properties = propCollection; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)media).ResetDirtyProperties(false); - return media; - } - /// /// Private method to create a media object from a ContentDto /// @@ -519,15 +493,10 @@ namespace Umbraco.Core.Persistence.Repositories private IMedia CreateMediaFromDto(ContentVersionDto dto, Guid versionId) { var contentType = _mediaTypeRepository.Get(dto.ContentDto.ContentTypeId); - - var factory = new MediaFactory(contentType, NodeObjectTypeId, dto.NodeId); - var media = factory.BuildEntity(dto); - - var docDef = new DocumentDefinition(dto.NodeId, versionId, media.UpdateDate, media.CreateDate, contentType); - - var properties = GetPropertyCollection(new[] { docDef }); - - media.Properties = properties[dto.NodeId]; + var media = MediaFactory.BuildEntity(dto, contentType); + var temp = new TempContent(dto.NodeId, versionId, media.UpdateDate, media.CreateDate, contentType); + var properties = GetPropertyCollection(new List { temp }); + media.Properties = properties[versionId]; //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaTypeContainerRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaTypeContainerRepository.cs index 1cdecc8da6..969fef118f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaTypeContainerRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaTypeContainerRepository.cs @@ -1,13 +1,12 @@ using Umbraco.Core.Cache; using Umbraco.Core.Logging; -using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { class MediaTypeContainerRepository : EntityContainerRepository, IMediaTypeContainerRepository { - public MediaTypeContainerRepository(IDatabaseUnitOfWork uow, CacheHelper cache, ILogger logger) + public MediaTypeContainerRepository(IScopeUnitOfWork uow, CacheHelper cache, ILogger logger) : base(uow, cache, logger, Constants.ObjectTypes.MediaTypeContainerGuid) { } } diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs index 184964397d..7003f43fc6 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs @@ -18,20 +18,13 @@ namespace Umbraco.Core.Persistence.Repositories { private IRepositoryCachePolicy _cachePolicy; - public MediaTypeRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger) + public MediaTypeRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger) : base(work, cache, logger) { } - protected override IRepositoryCachePolicy CachePolicy + protected override IRepositoryCachePolicy CreateCachePolicy(IRuntimeCacheProvider runtimeCache) { - get - { - if (_cachePolicy != null) return _cachePolicy; - - _cachePolicy = new FullDataSetRepositoryCachePolicy(RuntimeCache, GetEntityId, /*expires:*/ true); - - return _cachePolicy; - } + return new FullDataSetRepositoryCachePolicy(runtimeCache, GetEntityId, /*expires:*/ true); } protected override IMediaType PerformGet(int id) diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberGroupRepository.cs index 081c786531..cd9631d21f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberGroupRepository.cs @@ -13,11 +13,9 @@ using Umbraco.Core.Cache; namespace Umbraco.Core.Persistence.Repositories { - - internal class MemberGroupRepository : NPocoRepositoryBase, IMemberGroupRepository { - public MemberGroupRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger) + public MemberGroupRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger) : base(work, cache, logger) { } @@ -120,7 +118,7 @@ namespace Umbraco.Core.Persistence.Repositories public IMemberGroup GetByName(string name) { - return RuntimeCache.GetCacheItem( + return IsolatedCache.GetCacheItem( typeof (IMemberGroup).FullName + "." + name, () => { @@ -147,13 +145,10 @@ namespace Umbraco.Core.Persistence.Repositories }; PersistNewItem(grp); - if (SavingMemberGroup.IsRaisedEventCancelled(new SaveEventArgs(grp), this)) - { + if (UnitOfWork.Events.DispatchCancelable(SavingMemberGroup, this, new SaveEventArgs(grp))) return null; - } - - SavedMemberGroup.RaiseEvent(new SaveEventArgs(grp), this); + UnitOfWork.Events.Dispatch(SavedMemberGroup, this, new SaveEventArgs(grp)); return grp; } @@ -258,15 +253,13 @@ namespace Umbraco.Core.Persistence.Repositories var missingRoles = roleNames.Except(existingRoles); var missingGroups = missingRoles.Select(x => new MemberGroup {Name = x}).ToArray(); - if (SavingMemberGroup.IsRaisedEventCancelled(new SaveEventArgs(missingGroups), this)) - { + if (UnitOfWork.Events.DispatchCancelable(SavingMemberGroup, this, new SaveEventArgs(missingGroups))) return; - } + foreach (var m in missingGroups) - { PersistNewItem(m); - } - SavedMemberGroup.RaiseEvent(new SaveEventArgs(missingGroups), this); + + UnitOfWork.Events.Dispatch(SavedMemberGroup, this, new SaveEventArgs(missingGroups)); //now go get all the dto's for roles with these role names var rolesForNames = Database.Fetch(existingSql).ToArray(); diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index 50d0784d45..06fbd1f168 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -2,22 +2,16 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Text; -using System.Xml.Linq; using NPoco; using Umbraco.Core.Cache; -using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models; using Umbraco.Core.Models.Rdbms; -using Umbraco.Core.Cache; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; -using Umbraco.Core.Persistence.Mappers; namespace Umbraco.Core.Persistence.Repositories { @@ -30,24 +24,15 @@ namespace Umbraco.Core.Persistence.Repositories private readonly ITagRepository _tagRepository; private readonly IMemberGroupRepository _memberGroupRepository; - public MemberRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, IContentSection contentSection) - : base(work, cache, logger, contentSection) + public MemberRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository) + : base(work, cache, logger) { - if (memberTypeRepository == null) throw new ArgumentNullException(nameof(memberTypeRepository)); - if (tagRepository == null) throw new ArgumentNullException(nameof(tagRepository)); - _memberTypeRepository = memberTypeRepository; - _tagRepository = tagRepository; + _memberTypeRepository = memberTypeRepository ?? throw new ArgumentNullException(nameof(memberTypeRepository)); + _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); _memberGroupRepository = memberGroupRepository; } - public void SetNoCachePolicy() - { - // using NoCache here means that we are NOT updating the cache - // so this should be OK for reads but NOT for writes! - CachePolicy = new NoCacheRepositoryCachePolicy(); - } - - protected override MemberRepository Instance => this; + protected override MemberRepository This => this; #region Overrides of RepositoryBase @@ -116,15 +101,30 @@ namespace Umbraco.Core.Persistence.Repositories #region Overrides of NPocoRepositoryBase protected override Sql GetBaseQuery(bool isCount) + { + return GetBaseQuery(isCount ? QueryType.Count : QueryType.Single); + } + + protected override Sql GetBaseQuery(QueryType queryType) { var sql = Sql(); - sql = isCount - ? sql.SelectCount() - : sql.Select(r => - r.Select(rr => - rr.Select(rrr => - rrr.Select()))); + switch (queryType) + { + case QueryType.Count: + sql = sql.SelectCount(); + break; + case QueryType.Ids: + sql = sql.Select("cmsMember.nodeId"); + break; + case QueryType.Many: + case QueryType.Single: + sql = sql.Select(r => + r.Select(rr => + rr.Select(rrr => + rrr.Select()))); + break; + } sql .From() @@ -141,7 +141,6 @@ namespace Umbraco.Core.Persistence.Repositories .Where(x => x.NodeObjectType == NodeObjectTypeId); return sql; - } protected override string GetBaseWhereClause() @@ -215,7 +214,7 @@ namespace Umbraco.Core.Persistence.Repositories 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); + var unused = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); //Update with new correct path nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); @@ -302,7 +301,7 @@ namespace Umbraco.Core.Persistence.Repositories //Updates the (base) node data - umbracoNode var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; - var o = Database.Update(nodeDto); + var unused = Database.Update(nodeDto); //Only update this DTO if the contentType has actually changed if (contentDto.ContentTypeId != ((Member)entity).ContentTypeId) @@ -411,18 +410,16 @@ namespace Umbraco.Core.Persistence.Repositories return null; var memberType = _memberTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); + var member = MemberFactory.BuildEntity(dto, memberType); - var factory = new MemberFactory(memberType, NodeObjectTypeId, dto.NodeId); - var media = factory.BuildEntity(dto); + var properties = GetPropertyCollection(new List { new TempContent(dto.NodeId, dto.ContentVersionDto.VersionId, member.UpdateDate, member.CreateDate, memberType) }); - var properties = GetPropertyCollection(new[] { new DocumentDefinition(dto.NodeId, dto.ContentVersionDto.VersionId, media.UpdateDate, media.CreateDate, memberType) }); - - media.Properties = properties[dto.NodeId]; + member.Properties = properties[dto.ContentVersionDto.VersionId]; //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 - ((Entity)media).ResetDirtyProperties(false); - return media; + ((Entity)member).ResetDirtyProperties(false); + return member; } @@ -553,15 +550,17 @@ namespace Umbraco.Core.Persistence.Repositories /// The query supplied will ONLY work with data specifically on the cmsMember table because we are using NPoco paging (SQL paging) /// public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, string filter = "") + string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter = null, bool newest = true) { - var filterSql = filter.IsNullOrWhiteSpace() - ? null - : Sql().Append(GetPagedResultsByQueryWhere(), $"%{filter}%"); - - // note: need to test whether NPoco gets confused by the same parameter being used twice, - // as PetaPoco supposedly was, in which case we'd need to use two parameters. in any case, - // better to create the query text only once! + Sql filterSql = null; + if (filter != null) + { + filterSql = Sql(); + foreach (var clause in filter.GetWhereClauses()) + { + filterSql = filterSql.Append($"AND ({clause.Item1})", clause.Item2); + } + } return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, x => MapQueryDtos(x), orderBy, orderDirection, orderBySystemField, "cmsMember", @@ -601,17 +600,18 @@ namespace Umbraco.Core.Persistence.Repositories private IEnumerable MapQueryDtos(List dtos, bool withCache = false) { var content = new IMember[dtos.Count]; - var defs = new List(); + var temps = new List(); + var contentTypes = new Dictionary(); for (var i = 0; i < dtos.Count; i++) { var dto = dtos[i]; - // if the cache contains the item, use it if (withCache) { - var cached = RuntimeCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); - if (cached != null) + // if the cache contains the (proper version of the) item, use it + var cached = IsolatedCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); + if (cached != null && cached.Version == dto.ContentVersionDto.VersionId) { content[i] = cached; continue; @@ -619,38 +619,46 @@ namespace Umbraco.Core.Persistence.Repositories } // else, need to fetch from the database - // content type repository is full-cache so OK to get each one independently - var contentType = _memberTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); - var factory = new MemberFactory(contentType, NodeObjectTypeId, dto.NodeId); - content[i] = factory.BuildEntity(dto); + + // 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 + if (contentTypes.TryGetValue(dto.ContentVersionDto.ContentDto.ContentTypeId, out IMemberType contentType) == false) + contentTypes[dto.ContentVersionDto.ContentDto.ContentTypeId] = contentType = _memberTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); + + // fixme + // + // 7.6 ProcessQuery has an additional 'allVersions' flag that is false by default, meaning + // we should always get the latest version of each content item. meaning what what we + // are processing now is a more recent version than what we already processed, we need to + // replace + // but it has flaws: it's not dealing with what could be in the cache (should be the latest + // version, always) and it's not replacing the content that is already in the list... + // so considering it broken, not implementing now, MUST FIX + + var c = content[i] = MemberFactory.BuildEntity(dto, contentType); // need properties - defs.Add(new DocumentDefinition( + temps.Add(new TempContent( dto.NodeId, dto.ContentVersionDto.VersionId, dto.ContentVersionDto.VersionDate, dto.ContentVersionDto.ContentDto.NodeDto.CreateDate, - contentType + contentType, + c )); } // load all properties for all documents from database in 1 query - var propertyData = GetPropertyCollection(defs.ToArray()); + var propertyData = GetPropertyCollection(temps); // assign - var dtoIndex = 0; - foreach (var def in defs) + foreach (var temp in temps) { - // move to corresponding item (which has to exist) - while (dtos[dtoIndex].NodeId != def.Id) dtoIndex++; - - // complete the item - var cc = content[dtoIndex]; - cc.Properties = propertyData[cc.Id]; + temp.Content.Properties = propertyData[temp.Version]; //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 - ((Entity)cc).ResetDirtyProperties(false); + ((Entity) temp.Content).ResetDirtyProperties(false); } return content; @@ -665,15 +673,11 @@ namespace Umbraco.Core.Persistence.Repositories private IMember CreateMemberFromDto(MemberDto dto, Guid versionId) { var memberType = _memberTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); + var member = MemberFactory.BuildEntity(dto, memberType); + var temp = new TempContent(dto.ContentVersionDto.NodeId, versionId, member.UpdateDate, member.CreateDate, memberType); - var factory = new MemberFactory(memberType, NodeObjectTypeId, dto.ContentVersionDto.NodeId); - var member = factory.BuildEntity(dto); - - var docDef = new DocumentDefinition(dto.ContentVersionDto.NodeId, versionId, member.UpdateDate, member.CreateDate, memberType); - - var properties = GetPropertyCollection(new[] { docDef }); - - member.Properties = properties[dto.ContentVersionDto.NodeId]; + var properties = GetPropertyCollection(new List { temp }); + member.Properties = properties[dto.ContentVersionDto.VersionId]; //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs index fc38520936..02cf1c7b9d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs @@ -21,20 +21,13 @@ namespace Umbraco.Core.Persistence.Repositories { private IRepositoryCachePolicy _cachePolicy; - public MemberTypeRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger) + public MemberTypeRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger) : base(work, cache, logger) { } - protected override IRepositoryCachePolicy CachePolicy + protected override IRepositoryCachePolicy CreateCachePolicy(IRuntimeCacheProvider runtimeCache) { - get - { - if (_cachePolicy != null) return _cachePolicy; - - _cachePolicy = new FullDataSetRepositoryCachePolicy(RuntimeCache, GetEntityId, /*expires:*/ true); - - return _cachePolicy; - } + return new FullDataSetRepositoryCachePolicy(runtimeCache, GetEntityId, /*expires:*/ true); } protected override IMemberType PerformGet(int id) diff --git a/src/Umbraco.Core/Persistence/Repositories/MigrationEntryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MigrationEntryRepository.cs index c4b1f25fc3..5e27025923 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MigrationEntryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MigrationEntryRepository.cs @@ -17,7 +17,7 @@ namespace Umbraco.Core.Persistence.Repositories { internal class MigrationEntryRepository : NPocoRepositoryBase, IMigrationEntryRepository { - public MigrationEntryRepository(IDatabaseUnitOfWork work, [Inject(RepositoryCompositionRoot.DisabledCache)] CacheHelper cache, ILogger logger) + public MigrationEntryRepository(IScopeUnitOfWork work, [Inject(RepositoryCompositionRoot.DisabledCache)] CacheHelper cache, ILogger logger) : base(work, cache, logger) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/NPocoRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/NPocoRepositoryBase.cs index a16b9ea576..c3622d6532 100644 --- a/src/Umbraco.Core/Persistence/Repositories/NPocoRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/NPocoRepositoryBase.cs @@ -24,14 +24,14 @@ namespace Umbraco.Core.Persistence.Repositories /// A database unit of work. /// A cache helper. /// A logger. - protected NPocoRepositoryBase(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger) + protected NPocoRepositoryBase(IScopeUnitOfWork work, CacheHelper cache, ILogger logger) : base(work, cache, logger) { } /// /// Gets the repository's unit of work. /// - protected internal new IDatabaseUnitOfWork UnitOfWork => (IDatabaseUnitOfWork) base.UnitOfWork; + protected internal new IScopeUnitOfWork UnitOfWork => base.UnitOfWork; /// /// Gets the repository's database. @@ -56,7 +56,7 @@ namespace Umbraco.Core.Persistence.Repositories #region Abstract Methods - protected abstract Sql GetBaseQuery(bool isCount); + protected abstract Sql GetBaseQuery(bool isCount); // fixme obsolete, use QueryType instead everywhere protected abstract string GetBaseWhereClause(); protected abstract IEnumerable GetDeleteClauses(); protected abstract Guid NodeObjectTypeId { get; } @@ -90,10 +90,5 @@ namespace Umbraco.Core.Persistence.Repositories Database.Execute(delete, new { Id = GetEntityId(entity) }); } } - - protected new virtual TId GetEntityId(TEntity entity) - { - return (TId)(object) entity.Id; - } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs index e83c65c592..4ff81a210f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs @@ -22,10 +22,10 @@ namespace Umbraco.Core.Persistence.Repositories internal class PermissionRepository where TEntity : class, IAggregateRoot { - private readonly IDatabaseUnitOfWork _unitOfWork; + private readonly IScopeUnitOfWork _unitOfWork; private readonly IRuntimeCacheProvider _runtimeCache; - internal PermissionRepository(IDatabaseUnitOfWork unitOfWork, CacheHelper cache) + internal PermissionRepository(IScopeUnitOfWork unitOfWork, CacheHelper cache) { _unitOfWork = unitOfWork; //Make this repository use an isolated cache @@ -148,8 +148,7 @@ namespace Umbraco.Core.Persistence.Repositories db.BulkInsertRecords(toInsert); //Raise the event - AssignedPermissions.RaiseEvent( - new SaveEventArgs(ConvertToPermissionList(toInsert), false), this); + _unitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(ConvertToPermissionList(toInsert), false)); } /// @@ -180,8 +179,7 @@ namespace Umbraco.Core.Persistence.Repositories db.BulkInsertRecords(actions); //Raise the event - AssignedPermissions.RaiseEvent( - new SaveEventArgs(ConvertToPermissionList(actions), false), this); + _unitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(ConvertToPermissionList(actions), false)); } /// @@ -212,8 +210,7 @@ namespace Umbraco.Core.Persistence.Repositories db.BulkInsertRecords(actions); //Raise the event - AssignedPermissions.RaiseEvent( - new SaveEventArgs(ConvertToPermissionList(actions), false), this); + _unitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(ConvertToPermissionList(actions), false)); } /// @@ -240,8 +237,7 @@ namespace Umbraco.Core.Persistence.Repositories db.BulkInsertRecords(actions); //Raise the event - AssignedPermissions.RaiseEvent( - new SaveEventArgs(ConvertToPermissionList(actions), false), this); + _unitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(ConvertToPermissionList(actions), false)); } private static IEnumerable ConvertToPermissionList(IEnumerable result) diff --git a/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs index 8542a2cae0..86b8f39a5c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs @@ -14,22 +14,13 @@ namespace Umbraco.Core.Persistence.Repositories { internal class PublicAccessRepository : NPocoRepositoryBase, IPublicAccessRepository { - private IRepositoryCachePolicy _cachePolicy; - - public PublicAccessRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger) + public PublicAccessRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger) : base(work, cache, logger) { } - protected override IRepositoryCachePolicy CachePolicy + protected override IRepositoryCachePolicy CreateCachePolicy(IRuntimeCacheProvider runtimeCache) { - get - { - if (_cachePolicy != null) return _cachePolicy; - - _cachePolicy = new FullDataSetRepositoryCachePolicy(RuntimeCache, GetEntityId, /*expires:*/ false); - - return _cachePolicy; - } + return new FullDataSetRepositoryCachePolicy(runtimeCache, GetEntityId, /*expires:*/ false); } protected override PublicAccessEntry PerformGet(Guid id) @@ -44,7 +35,7 @@ namespace Umbraco.Core.Persistence.Repositories if (ids.Any()) { - sql.Where("umbracoAccess.id IN (@ids)", new { ids = ids }); + sql.Where("umbracoAccess.id IN (@ids)", new { ids }); } sql.OrderBy(x => x.NodeId); @@ -89,15 +80,13 @@ namespace Umbraco.Core.Persistence.Repositories return list; } - protected override Guid NodeObjectTypeId - { - get { throw new NotImplementedException(); } - } + protected override Guid NodeObjectTypeId => throw new NotImplementedException(); protected override void PersistNewItem(PublicAccessEntry entity) { entity.AddingEntity(); - entity.Rules.ForEach(x => x.AddingEntity()); + foreach (var rule in entity.Rules) + rule.AddingEntity(); var factory = new PublicAccessEntryFactory(); var dto = factory.BuildDto(entity); @@ -118,8 +107,13 @@ namespace Umbraco.Core.Persistence.Repositories protected override void PersistUpdatedItem(PublicAccessEntry entity) { entity.UpdatingEntity(); - entity.Rules.Where(x => x.HasIdentity).ForEach(x => x.UpdatingEntity()); - entity.Rules.Where(x => x.HasIdentity == false).ForEach(x => x.AddingEntity()); + foreach (var rule in entity.Rules) + { + if (rule.HasIdentity) + rule.UpdatingEntity(); + else + rule.AddingEntity(); + } var factory = new PublicAccessEntryFactory(); var dto = factory.BuildDto(entity); diff --git a/src/Umbraco.Core/Persistence/Repositories/QueryType.cs b/src/Umbraco.Core/Persistence/Repositories/QueryType.cs new file mode 100644 index 0000000000..319a24f6d7 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/QueryType.cs @@ -0,0 +1,28 @@ +namespace Umbraco.Core.Persistence.Repositories +{ + /// + /// Specifies the type of base query. + /// + internal enum QueryType + { + /// + /// Get one single complete item. + /// + Single, + + /// + /// Get many complete items. + /// + Many, + + /// + /// Get item identifiers only. + /// + Ids, + + /// + /// Count items. + /// + Count + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs b/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs index 06b136ed31..325bdcb0eb 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs @@ -3,8 +3,6 @@ using Umbraco.Core.Cache; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; using Umbraco.Core.Models.EntityBase; -using Umbraco.Core.Persistence.Mappers; -using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories @@ -13,10 +11,9 @@ namespace Umbraco.Core.Persistence.Repositories where TEntity : class, IUmbracoEntity where TRepository : class, IRepository { - protected RecycleBinRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, IContentSection contentSection) - : base(work, cache, logger, contentSection) - { - } + protected RecycleBinRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger /*, IContentSection contentSection*/) + : base(work, cache, logger /*, contentSection*/) + { } protected abstract int RecycleBinId { get; } diff --git a/src/Umbraco.Core/Persistence/Repositories/RedirectUrlRepository.cs b/src/Umbraco.Core/Persistence/Repositories/RedirectUrlRepository.cs index 122528a785..48f4ef6e12 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RedirectUrlRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RedirectUrlRepository.cs @@ -13,7 +13,7 @@ namespace Umbraco.Core.Persistence.Repositories { internal class RedirectUrlRepository : NPocoRepositoryBase, IRedirectUrlRepository { - public RedirectUrlRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger) + public RedirectUrlRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger) : base(work, cache, logger) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/RelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/RelationRepository.cs index c9a85ecce4..104e5c41c5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RelationRepository.cs @@ -23,7 +23,7 @@ namespace Umbraco.Core.Persistence.Repositories { private readonly IRelationTypeRepository _relationTypeRepository; - public RelationRepository(IDatabaseUnitOfWork work, [Inject(RepositoryCompositionRoot.DisabledCache)] CacheHelper cache, ILogger logger, IRelationTypeRepository relationTypeRepository) + public RelationRepository(IScopeUnitOfWork work, [Inject(RepositoryCompositionRoot.DisabledCache)] CacheHelper cache, ILogger logger, IRelationTypeRepository relationTypeRepository) : base(work, cache, logger) { _relationTypeRepository = relationTypeRepository; @@ -81,7 +81,7 @@ namespace Umbraco.Core.Persistence.Repositories if (relationTypeId != x.RelationType) factory = new RelationFactory(_relationTypeRepository.Get(relationTypeId = x.RelationType)); return DtoToEntity(x, factory); - }); + }).ToList(); } private static IRelation DtoToEntity(RelationDto dto, RelationFactory factory) diff --git a/src/Umbraco.Core/Persistence/Repositories/RelationTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/RelationTypeRepository.cs index bbcca4e12e..6d6aad3ced 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RelationTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RelationTypeRepository.cs @@ -18,24 +18,13 @@ namespace Umbraco.Core.Persistence.Repositories /// internal class RelationTypeRepository : NPocoRepositoryBase, IRelationTypeRepository { - private IRepositoryCachePolicy _cachePolicy; - - public RelationTypeRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger) + public RelationTypeRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger) : base(work, cache, logger) { } - protected override IRepositoryCachePolicy CachePolicy + protected override IRepositoryCachePolicy CreateCachePolicy(IRuntimeCacheProvider runtimeCache) { - get - { - if (_cachePolicy != null) return _cachePolicy; - - // assuming we don't have tons of relation types, use a FullDataSet policy, ie - // cache the entire GetAll result once in a single collection - which can expire - _cachePolicy = new FullDataSetRepositoryCachePolicy(RuntimeCache, GetEntityId, /*expires:*/ true); - - return _cachePolicy; - } + return new FullDataSetRepositoryCachePolicy(runtimeCache, GetEntityId, /*expires:*/ true); } #region Overrides of RepositoryBase @@ -46,6 +35,17 @@ namespace Umbraco.Core.Persistence.Repositories return GetAll().FirstOrDefault(x => x.Id == id); } + public IRelationType Get(Guid id) + { + // use the underlying GetAll which will force cache all content types + return GetAll().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); @@ -59,6 +59,15 @@ namespace Umbraco.Core.Persistence.Repositories return dtos.Select(x => DtoToEntity(x, factory)); } + public IEnumerable GetAll(params Guid[] ids) + { + // should not happen due to the cache policy + if (ids.Any()) + throw new NotImplementedException(); + + return GetAll(new int[0]); + } + protected override IEnumerable PerformGetByQuery(IQuery query) { var sqlClause = GetBaseQuery(false); diff --git a/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs index 30d7d4a59a..cfc84de220 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs @@ -1,11 +1,7 @@ using System; using System.Collections.Generic; -using System.Linq; using Umbraco.Core.Cache; using Umbraco.Core.Logging; -using Umbraco.Core.Models.EntityBase; - -using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories @@ -17,21 +13,18 @@ namespace Umbraco.Core.Persistence.Repositories { private static readonly Dictionary CacheTypeKeys = new Dictionary(); - protected RepositoryBase(IUnitOfWork work, CacheHelper cache, ILogger logger) + protected RepositoryBase(IScopeUnitOfWork work, CacheHelper cache, ILogger logger) { - if (work == null) throw new ArgumentNullException(nameof(work)); - if (cache == null) throw new ArgumentNullException(nameof(cache)); - if (logger == null) throw new ArgumentNullException(nameof(logger)); - Logger = logger; - UnitOfWork = work; - Cache = cache; + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + UnitOfWork = work ?? throw new ArgumentNullException(nameof(work)); + GlobalCache = cache ?? throw new ArgumentNullException(nameof(cache)); } - protected IUnitOfWork UnitOfWork { get; } + protected IScopeUnitOfWork UnitOfWork { get; } - protected CacheHelper Cache { get; } + protected CacheHelper GlobalCache { get; } - protected virtual IRuntimeCacheProvider RuntimeCache => Cache.RuntimeCache; + protected abstract IRuntimeCacheProvider IsolatedCache { get; } protected ILogger Logger { get; } @@ -39,190 +32,8 @@ namespace Umbraco.Core.Persistence.Repositories public static string GetCacheTypeKey() { - string key; - var type = typeof (T); - return CacheTypeKeys.TryGetValue(type, out key) ? key : (CacheTypeKeys[type] = "uRepo_" + type.Name + "_"); - } - } - - /// - /// Provides a base class to all repositories. - /// - /// The type of the entity managed by this repository. - /// The type of the entity's unique identifier. - internal abstract class RepositoryBase : RepositoryBase, IReadRepository, IWriteRepository, IQueryRepository, IUnitOfWorkRepository - where TEntity : class, IAggregateRoot - { - private IRepositoryCachePolicy _cachePolicy; - - protected RepositoryBase(IUnitOfWork work, CacheHelper cache, ILogger logger) - : base(work, cache, logger) - { } - - protected override IRuntimeCacheProvider RuntimeCache => Cache.IsolatedRuntimeCache.GetOrCreateCache(); - - /// - /// Creates a new query. - /// - public virtual IQuery QueryT => Query(); - - /// - /// Creates a new query. - /// - public abstract IQuery Query(); - - #region Static Queries - - private IQuery _hasIdQuery; - - #endregion - - protected virtual TId GetEntityId(TEntity entity) - { - return (TId) (object) entity.Id; - } - - protected virtual IRepositoryCachePolicy CachePolicy - { - get - { - if (_cachePolicy != null) return _cachePolicy; - - var options = new RepositoryCachePolicyOptions(() => - { - // have to initialize query here, because some TEntity cannot create a query, - // but for those TEntity we don't perform counts? all this is a bit weird - if (_hasIdQuery == null) _hasIdQuery = QueryT.Where(x => x.Id != 0); - return PerformCount(_hasIdQuery); - }); - return _cachePolicy = new DefaultRepositoryCachePolicy(RuntimeCache, options); - } - set - { - _cachePolicy = value; // ok to set to null, reverts to default - } - } - - /// - /// Adds or Updates an entity of type TEntity - /// - /// This method is backed by an cache - /// - public void AddOrUpdate(TEntity entity) - { - if (entity.HasIdentity == false) - UnitOfWork.RegisterCreated(entity, this); - else - UnitOfWork.RegisterUpdated(entity, this); - } - - /// - /// Deletes the passed in entity - /// - /// - public virtual void Delete(TEntity entity) - { - UnitOfWork?.RegisterDeleted(entity, this); - } - - protected abstract TEntity PerformGet(TId id); - /// - /// Gets an entity by the passed in Id utilizing the repository's cache policy - /// - /// - /// - public TEntity Get(TId id) - { - return CachePolicy.Get(id, PerformGet, PerformGetAll); - } - - protected abstract IEnumerable PerformGetAll(params TId[] ids); - /// - /// Gets all entities of type TEntity or a list according to the passed in Ids - /// - /// - /// - public IEnumerable GetAll(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(); - - if (ids.Length > 2000) - { - throw new InvalidOperationException("Cannot perform a query with more than 2000 parameters"); - } - - return CachePolicy.GetAll(ids, PerformGetAll); - } - - protected abstract IEnumerable PerformGetByQuery(IQuery query); - protected abstract bool PerformExists(TId id); - protected abstract int PerformCount(IQuery query); - protected abstract void PersistNewItem(TEntity item); - protected abstract void PersistUpdatedItem(TEntity item); - protected abstract void PersistDeletedItem(TEntity item); - - /// - /// Gets a list of entities by the passed in query - /// - /// - /// - public IEnumerable GetByQuery(IQuery query) - { - return PerformGetByQuery(query) - //ensure we don't include any null refs in the returned collection! - .WhereNotNull(); - } - - /// - /// Returns a boolean indicating whether an entity with the passed Id exists - /// - /// - /// - public bool Exists(TId id) - { - return CachePolicy.Exists(id, PerformExists, PerformGetAll); - } - - /// - /// Returns an integer with the count of entities found with the passed in query - /// - /// - /// - public int Count(IQuery query) - { - return PerformCount(query); - } - - /// - /// Unit of work method that tells the repository to persist the new entity - /// - /// - public virtual void PersistNewItem(IEntity entity) - { - CachePolicy.Create((TEntity) entity, PersistNewItem); - } - - /// - /// Unit of work method that tells the repository to persist the updated entity - /// - /// - public virtual void PersistUpdatedItem(IEntity entity) - { - CachePolicy.Update((TEntity) entity, PersistUpdatedItem); - } - - /// - /// Unit of work method that tells the repository to persist the deletion of the entity - /// - /// - public virtual void PersistDeletedItem(IEntity entity) - { - CachePolicy.Delete((TEntity) entity, PersistDeletedItem); + var type = typeof(T); + return CacheTypeKeys.TryGetValue(type, out string key) ? key : (CacheTypeKeys[type] = "uRepo_" + type.Name + "_"); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/RepositoryBaseOfTIdTEntity.cs b/src/Umbraco.Core/Persistence/Repositories/RepositoryBaseOfTIdTEntity.cs new file mode 100644 index 0000000000..933124e11c --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/RepositoryBaseOfTIdTEntity.cs @@ -0,0 +1,280 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Cache; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.UnitOfWork; +using Umbraco.Core.Scoping; + +namespace Umbraco.Core.Persistence.Repositories +{ + /// + /// Provides a base class to all repositories. + /// + /// The type of the entity managed by this repository. + /// The type of the entity's unique identifier. + internal abstract class RepositoryBase : RepositoryBase, IReadRepository, IWriteRepository, IQueryRepository, IUnitOfWorkRepository + where TEntity : class, IAggregateRoot + { + private IRepositoryCachePolicy _cachePolicy; + + protected RepositoryBase(IScopeUnitOfWork work, CacheHelper cache, ILogger logger) + : base(work, cache, logger) + { } + + /// + /// Creates a new query. + /// + public virtual IQuery QueryT => Query(); + + /// + /// Creates a new query. + /// + public abstract IQuery Query(); + + #region Static Queries + + private IQuery _hasIdQuery; + + #endregion + + protected virtual TId GetEntityId(TEntity entity) + { + return (TId) (object) entity.Id; + } + + /// + /// The runtime cache used for this repo by default is the isolated cache for this type + /// + private IRuntimeCacheProvider _isolatedCache; + protected override IRuntimeCacheProvider IsolatedCache + { + get + { + if (_isolatedCache != null) return _isolatedCache; + + var scope = UnitOfWork.Scope; + IsolatedRuntimeCache provider; + switch (scope.RepositoryCacheMode) + { + case RepositoryCacheMode.Default: + provider = GlobalCache.IsolatedRuntimeCache; + break; + case RepositoryCacheMode.Scoped: + provider = scope.IsolatedRuntimeCache; + break; + default: + throw new Exception("oops: cache mode."); + } + + return _isolatedCache = GetIsolatedCache(provider); + } + } + + protected virtual IRuntimeCacheProvider GetIsolatedCache(IsolatedRuntimeCache provider) + { + return provider.GetOrCreateCache(); + } + + // this is a *bad* idea because PerformCount captures the current repository and its UOW + // + //private static RepositoryCachePolicyOptions _defaultOptions; + //protected virtual RepositoryCachePolicyOptions DefaultOptions + //{ + // get + // { + // return _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! + // var query = _hasIdQuery ?? (_hasIdQuery = Query.Builder.Where(x => x.Id != 0)); + // return PerformCount(query); + // })); + // } + //} + + protected virtual RepositoryCachePolicyOptions DefaultOptions + { + get + { + return 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! + var query = _hasIdQuery ?? (_hasIdQuery = QueryT.Where(x => x.Id != 0)); + return PerformCount(query); + }); + } + } + + // this would be better for perfs BUT it breaks the tests - l8tr + // + //private static IRepositoryCachePolicy _defaultCachePolicy; + //protected virtual IRepositoryCachePolicy DefaultCachePolicy + //{ + // get + // { + // return _defaultCachePolicy ?? (_defaultCachePolicy + // = new DefaultRepositoryCachePolicy(IsolatedCache, DefaultOptions)); + // } + //} + + protected virtual IRepositoryCachePolicy CachePolicy + { + get + { + if (GlobalCache == CacheHelper.NoCache) + return _cachePolicy = NoCacheRepositoryCachePolicy.Instance; + + // create the cache policy using IsolatedCache which is either global + // or scoped depending on the repository cache mode for the current scope + _cachePolicy = CreateCachePolicy(IsolatedCache); + var scope = UnitOfWork.Scope; + switch (scope.RepositoryCacheMode) + { + case RepositoryCacheMode.Default: + break; + case RepositoryCacheMode.Scoped: + var globalIsolatedCache = GetIsolatedCache(GlobalCache.IsolatedRuntimeCache); + _cachePolicy = _cachePolicy.Scoped(globalIsolatedCache, scope); + break; + default: + throw new Exception("oops: cache mode."); + } + + return _cachePolicy; + } + } + + protected virtual IRepositoryCachePolicy CreateCachePolicy(IRuntimeCacheProvider runtimeCache) + { + return new DefaultRepositoryCachePolicy(runtimeCache, DefaultOptions); + } + + /// + /// Adds or Updates an entity of type TEntity + /// + /// This method is backed by an cache + /// + public void AddOrUpdate(TEntity entity) + { + if (entity.HasIdentity == false) + UnitOfWork.RegisterCreated(entity, this); + else + UnitOfWork.RegisterUpdated(entity, this); + } + + /// + /// Deletes the passed in entity + /// + /// + public virtual void Delete(TEntity entity) + { + UnitOfWork?.RegisterDeleted(entity, this); + } + + protected abstract TEntity PerformGet(TId id); + protected abstract IEnumerable PerformGetAll(params TId[] ids); + protected abstract IEnumerable PerformGetByQuery(IQuery query); + protected abstract bool PerformExists(TId id); + protected abstract int PerformCount(IQuery query); + protected abstract void PersistNewItem(TEntity item); + protected abstract void PersistUpdatedItem(TEntity item); + protected abstract void PersistDeletedItem(TEntity item); + + + /// + /// Gets an entity by the passed in Id utilizing the repository's cache policy + /// + /// + /// + public TEntity Get(TId id) + { + return CachePolicy.Get(id, PerformGet, PerformGetAll); + } + + /// + /// Gets all entities of type TEntity or a list according to the passed in Ids + /// + /// + /// + public IEnumerable GetAll(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(); + + if (ids.Length > 2000) + { + throw new InvalidOperationException("Cannot perform a query with more than 2000 parameters"); + } + + return CachePolicy.GetAll(ids, PerformGetAll); + } + + /// + /// Gets a list of entities by the passed in query + /// + /// + /// + public IEnumerable GetByQuery(IQuery query) + { + return PerformGetByQuery(query) + //ensure we don't include any null refs in the returned collection! + .WhereNotNull(); + } + + /// + /// Returns a boolean indicating whether an entity with the passed Id exists + /// + /// + /// + public bool Exists(TId id) + { + return CachePolicy.Exists(id, PerformExists, PerformGetAll); + } + + /// + /// Returns an integer with the count of entities found with the passed in query + /// + /// + /// + public int Count(IQuery query) + { + return PerformCount(query); + } + + /// + /// Unit of work method that tells the repository to persist the new entity + /// + /// + public virtual void PersistNewItem(IEntity entity) + { + CachePolicy.Create((TEntity) entity, PersistNewItem); + } + + /// + /// Unit of work method that tells the repository to persist the updated entity + /// + /// + public virtual void PersistUpdatedItem(IEntity entity) + { + CachePolicy.Update((TEntity) entity, PersistUpdatedItem); + } + + /// + /// Unit of work method that tells the repository to persist the deletion of the entity + /// + /// + public virtual void PersistDeletedItem(IEntity entity) + { + CachePolicy.Delete((TEntity) entity, PersistDeletedItem); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs index feb4add2d6..c2e1df7055 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs @@ -14,14 +14,23 @@ namespace Umbraco.Core.Persistence.Repositories { internal class ServerRegistrationRepository : NPocoRepositoryBase, IServerRegistrationRepository { - private IRepositoryCachePolicy _cachePolicy; - - public ServerRegistrationRepository(IDatabaseUnitOfWork work, CacheHelper cacheHelper, ILogger logger) + // fixme - should we use NoCache instead of CreateDisabledCacheHelper?! + public ServerRegistrationRepository(IScopeUnitOfWork work, ILogger logger) : base(work, CacheHelper.CreateDisabledCacheHelper(), logger) { } - protected override IRepositoryCachePolicy CachePolicy => _cachePolicy - ?? (_cachePolicy = new FullDataSetRepositoryCachePolicy(RuntimeCache, GetEntityId, /*expires:*/ false)); + protected override IRepositoryCachePolicy CreateCachePolicy(IRuntimeCacheProvider runtimeCache) + { + // fixme - wtf 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(GlobalCache.RuntimeCache, GetEntityId, /*expires:*/ false); + } public void ClearCache() { @@ -80,15 +89,12 @@ namespace Umbraco.Core.Persistence.Repositories { var list = new List { - "DELETE FROM umbracoServer WHERE id = @Id" + "DELETE FROM umbracoServer WHERE id = @Id" }; return list; } - protected override Guid NodeObjectTypeId - { - get { throw new NotImplementedException(); } - } + protected override Guid NodeObjectTypeId => throw new NotImplementedException(); protected override void PersistNewItem(IServerRegistration entity) { diff --git a/src/Umbraco.Core/Persistence/Repositories/SimpleGetRepository.cs b/src/Umbraco.Core/Persistence/Repositories/SimpleGetRepository.cs index 8598284b79..c52575693f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/SimpleGetRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/SimpleGetRepository.cs @@ -18,7 +18,7 @@ namespace Umbraco.Core.Persistence.Repositories where TDto: class { - protected SimpleGetRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger) + protected SimpleGetRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger) : base(work, cache, logger) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/StylesheetRepository.cs b/src/Umbraco.Core/Persistence/Repositories/StylesheetRepository.cs index b3dd90725e..3117bdce8a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/StylesheetRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/StylesheetRepository.cs @@ -144,6 +144,20 @@ namespace Umbraco.Core.Persistence.Repositories FileSystem.AddFile(filepath, content, true); } + public long GetFileSize(string filepath) + { + if (FileSystem.FileExists(filepath) == false) return -1; + + try + { + return FileSystem.GetSize(filepath); + } + catch + { + return -1; // deal with race conds + } + } + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/TagRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TagRepository.cs index d0872ccbd2..63611d0ad5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TagRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TagRepository.cs @@ -16,7 +16,7 @@ namespace Umbraco.Core.Persistence.Repositories { internal class TagRepository : NPocoRepositoryBase, ITagRepository { - public TagRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger) + public TagRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger) : base(work, cache, logger) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/TaskRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TaskRepository.cs index f7f0e4a3f4..1abe5606d6 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TaskRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TaskRepository.cs @@ -16,7 +16,7 @@ namespace Umbraco.Core.Persistence.Repositories { internal class TaskRepository : NPocoRepositoryBase, ITaskRepository { - public TaskRepository(IDatabaseUnitOfWork work, [Inject(RepositoryCompositionRoot.DisabledCache)] CacheHelper cache, ILogger logger) + public TaskRepository(IScopeUnitOfWork work, [Inject(RepositoryCompositionRoot.DisabledCache)] CacheHelper cache, ILogger logger) : base(work, cache, logger) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/TaskTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TaskTypeRepository.cs index 5a824ca14a..36869583b7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TaskTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TaskTypeRepository.cs @@ -16,7 +16,7 @@ namespace Umbraco.Core.Persistence.Repositories { internal class TaskTypeRepository : NPocoRepositoryBase, ITaskTypeRepository { - public TaskTypeRepository(IDatabaseUnitOfWork work, [Inject(RepositoryCompositionRoot.DisabledCache)] CacheHelper cache, ILogger logger) + public TaskTypeRepository(IScopeUnitOfWork work, [Inject(RepositoryCompositionRoot.DisabledCache)] CacheHelper cache, ILogger logger) : base(work, cache, logger) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs index 2e4443db56..afbc57c3b2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs @@ -1,12 +1,10 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Text; using NPoco; using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -15,13 +13,9 @@ using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence.Factories; -using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; -using Umbraco.Core.Services; using Umbraco.Core.Strings; -using Umbraco.Core.Sync; namespace Umbraco.Core.Persistence.Repositories { @@ -35,9 +29,8 @@ namespace Umbraco.Core.Persistence.Repositories private readonly ITemplatesSection _templateConfig; private readonly ViewHelper _viewHelper; private readonly MasterPageHelper _masterPageHelper; - private IRepositoryCachePolicy _cachePolicy; - public TemplateRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, IFileSystem masterpageFileSystem, IFileSystem viewFileSystem, ITemplatesSection templateConfig) + public TemplateRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, IFileSystem masterpageFileSystem, IFileSystem viewFileSystem, ITemplatesSection templateConfig) : base(work, cache, logger) { _masterpagesFileSystem = masterpageFileSystem; @@ -47,16 +40,9 @@ namespace Umbraco.Core.Persistence.Repositories _masterPageHelper = new MasterPageHelper(_masterpagesFileSystem); } - protected override IRepositoryCachePolicy CachePolicy + protected override IRepositoryCachePolicy CreateCachePolicy(IRuntimeCacheProvider runtimeCache) { - get - { - if (_cachePolicy != null) return _cachePolicy; - - _cachePolicy = new FullDataSetRepositoryCachePolicy(RuntimeCache, GetEntityId, /*expires:*/ false); - - return _cachePolicy; - } + return new FullDataSetRepositoryCachePolicy(runtimeCache, GetEntityId, /*expires:*/ false); } #region Overrides of RepositoryBase @@ -229,7 +215,12 @@ namespace Umbraco.Core.Persistence.Repositories { 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 @@ -286,12 +277,11 @@ namespace Umbraco.Core.Persistence.Repositories // once content has been set, "template on disk" are not "on disk" anymore template.Content = content; + SetVirtualPath(template); if (dto.Design == content) return; dto.Design = content; Database.Update(dto); // though... we don't care about the db value really??!! - - SetVirtualPath(template); } protected override void PersistDeletedItem(ITemplate entity) @@ -518,6 +508,11 @@ namespace Umbraco.Core.Persistence.Repositories GetFileSystem(filepath).AddFile(filepath, content, true); } + public long GetFileSize(string filepath) + { + return GetFileSystem(filepath).GetSize(filepath); + } + private IFileSystem GetFileSystem(string filepath) { var ext = Path.GetExtension(filepath); diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index 0872650e54..488c8eb6bd 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -30,7 +30,7 @@ namespace Umbraco.Core.Persistence.Repositories private readonly IMapperCollection _mapperCollection; private PermissionRepository _permissionRepository; - public UserRepository(IDatabaseUnitOfWork work, CacheHelper cacheHelper, ILogger logger, IUserTypeRepository userTypeRepository, IMapperCollection mapperCollection) + public UserRepository(IScopeUnitOfWork work, CacheHelper cacheHelper, ILogger logger, IUserTypeRepository userTypeRepository, IMapperCollection mapperCollection) : base(work, cacheHelper, logger) { _userTypeRepository = userTypeRepository; diff --git a/src/Umbraco.Core/Persistence/Repositories/UserTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserTypeRepository.cs index d2a87e9b89..8e22e91179 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserTypeRepository.cs @@ -18,7 +18,7 @@ namespace Umbraco.Core.Persistence.Repositories /// internal class UserTypeRepository : NPocoRepositoryBase, IUserTypeRepository { - public UserTypeRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger) + public UserTypeRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger) : base(work, cache, logger) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index c1def95616..d2275e918e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Text.RegularExpressions; using NPoco; using Umbraco.Core.Cache; -using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.DI; using Umbraco.Core.Events; using Umbraco.Core.Logging; @@ -16,32 +15,18 @@ using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; -using Umbraco.Core.Persistence.Mappers; namespace Umbraco.Core.Persistence.Repositories { - // this cannot be inside VersionableRepositoryBase because that class is static - internal static class VersionableRepositoryBaseAliasRegex + internal sealed class VersionableRepositoryBase { - private static readonly Dictionary Regexes = new Dictionary(); - - public static Regex For(ISqlSyntaxProvider sqlSyntax) - { - var type = sqlSyntax.GetType(); - Regex aliasRegex; - if (Regexes.TryGetValue(type, out aliasRegex)) - return aliasRegex; - - var col = Regex.Escape(sqlSyntax.GetQuotedColumnName("column")).Replace("column", @"\w+"); - var fld = Regex.Escape(sqlSyntax.GetQuotedTableName("table") + ".").Replace("table", @"\w+") + col; - aliasRegex = new Regex("(" + fld + @")\s+AS\s+(" + col + ")", RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.IgnoreCase); - Regexes[type] = aliasRegex; - return aliasRegex; - } + /// + /// This is used for unit tests ONLY + /// + public static bool ThrowOnWarning = false; } internal abstract class VersionableRepositoryBase : NPocoRepositoryBase @@ -50,13 +35,13 @@ namespace Umbraco.Core.Persistence.Repositories { //private readonly IContentSection _contentSection; - protected VersionableRepositoryBase(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, IContentSection contentSection) + protected VersionableRepositoryBase(IScopeUnitOfWork work, CacheHelper cache, ILogger logger /*, IContentSection contentSection*/) : base(work, cache, logger) { //_contentSection = contentSection; } - protected abstract TRepository Instance { get; } + protected abstract TRepository This { get; } #region IRepositoryVersionable Implementation @@ -305,12 +290,17 @@ namespace Umbraco.Core.Persistence.Repositories // 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 + // fixme - commented out is 7.6 and looks suspicious ??!! + //if (orderBySystemField && orderBy.InvariantEquals("umbraconode.id") == false) dbfield = GetDatabaseFieldNameForOrderBy("umbracoNode", "id"); - var matches = VersionableRepositoryBaseAliasRegex.For(SqlSyntax).Matches(sql.SQL); - var match = matches.Cast().FirstOrDefault(m => m.Groups[1].Value.InvariantEquals(dbfield)); - if (match != null) - dbfield = match.Groups[2].Value; - psql.OrderBy(dbfield); + if (orderBySystemField == false || orderBy.InvariantEquals(dbfield) == false) + { + var matches = VersionableRepositoryBaseAliasRegex.For(SqlSyntax).Matches(sql.SQL); + var match = matches.Cast().FirstOrDefault(m => m.Groups[1].Value.InvariantEquals(dbfield)); + if (match != null) + dbfield = match.Groups[2].Value; + psql.OrderBy(dbfield); + } // fixme - temp - for the time being NPoco PagingHelper cannot deal with multiline psql = new Sql(psql.SqlContext, psql.SQL.Replace("\r\n", " ").Replace("\r", " ").Replace("\n", " "), psql.Arguments); @@ -390,23 +380,10 @@ namespace Umbraco.Core.Persistence.Repositories WHERE cpt.Alias = @{sql.Arguments.Length}{andNewest}) AS CustomPropData ON CustomPropData.CustomPropValContentId = umbracoNode.id "; // trailing space is important! - // insert this just above the first LEFT OUTER JOIN (for cmsDocument) or the last WHERE (everything else) - string newSql; - if (table == "cmsDocument") - { - // insert the SQL fragment just above the LEFT OUTER JOIN [cmsDocument] [cmsDocument2] ... - // ensure it's there, 'cos, someone's going to edit the query, inevitably! - var pos = sql.SQL.InvariantIndexOf("LEFT OUTER JOIN"); - if (pos < 0) throw new Exception("Oops, LEFT OUTER JOIN not found."); - newSql = sql.SQL.Insert(pos, outerJoinTempTable); - } - else // anything else (see above) - { - // else same above WHERE - var pos = sql.SQL.InvariantIndexOf("WHERE"); - if (pos < 0) throw new Exception("Oops, WHERE not found."); - newSql = sql.SQL.Insert(pos, outerJoinTempTable); - } + // insert this just above the last WHERE + var pos = sql.SQL.InvariantIndexOf("WHERE"); + if (pos < 0) throw new Exception("Oops, WHERE not found."); + var newSql = sql.SQL.Insert(pos, outerJoinTempTable); var newArgs = sql.Arguments.ToList(); newArgs.Add(orderBy); @@ -421,6 +398,31 @@ namespace Umbraco.Core.Persistence.Repositories return "CustomPropData.CustomPropVal"; } + // fixme + // + // mergin from 7.6... well we cannot just merge what comes from 7.6 - too much distance + // + // need to understand what's been done in 7.6 and why, and reproduce here + // - 2a4e73c on 01/22 uses static factories, well we should use them everywhere but l8tr + // - c7b505f on 01/24 introduces QueryType and 2 queries for fetching content <<< why? + // because in 7.6, ProcessQuery calls GetPropertyCollection passing the sql which runs again + // but in v8 GetPropertyCollection has been refactored to use WhereIn() instead of a subquery + // as in most cases we won't get more than 2000 items -> nothing to do + // - 7f905bc on 01/26 fixes a "nasty" issue with reading properties + // - f192f24 on 01/31 fixes paged queries getting *all* property data + // - 86b2dac on 01/31 introduces the whole PagingQuery thing + // - 32d757b on 01/31 introduces the QueryType.Single vs .Many difference + // - af287c3 on 02/16 improves corruption handling for content (published/newest) + // - (more) on 02/22-27 fixes various minor issues + // + // so we want + // - to deal with the number of queries and getting *all* property data and outer joins + // - to deal with published/newest corruption + // + // but getting property data was already optimized, and we prob don't need the two queries thing + // so... doing our best but not really merging v7 + // see also Content/Member/Media repositories + protected IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, Func, IEnumerable> mapper, string orderBy, Direction orderDirection, bool orderBySystemField, string table, @@ -429,10 +431,8 @@ namespace Umbraco.Core.Persistence.Repositories if (orderBy == null) throw new ArgumentNullException(nameof(orderBy)); // start with base query, and apply the supplied IQuery - var sqlBase = GetBaseQuery(false); if (query == null) query = QueryT; - var translator = new SqlTranslator(sqlBase, query); - var sqlNodeIds = translator.Translate(); + var sqlNodeIds = new SqlTranslator(GetBaseQuery(QueryType.Many), query).Translate(); // sort and filter sqlNodeIds = PrepareSqlForPagedResults(sqlNodeIds, filterSql, orderBy, orderDirection, orderBySystemField, table); @@ -445,62 +445,364 @@ namespace Umbraco.Core.Persistence.Repositories return mapper(pagedResult.Items); } - protected IDictionary GetPropertyCollection(DocumentDefinition[] ddefs) + protected IDictionary GetPropertyCollection(List temps) { - var versions = ddefs.Select(x => x.Version).ToArray(); - if (versions.Length == 0) return new Dictionary(); + var versions = temps.Select(x => x.Version).ToArray(); + if (versions.Length == 0) return new Dictionary(); - // fetch by version only, that should be enough, versions are guids and the same guid - // should not be reused for two different nodes -- then validate with a Where() just - // to be sure -- but we probably can get rid of the validation - var allPropertyData = Database.FetchByGroups(versions, 2000, batch => + // get all PropertyDataDto for all definitions / versions + var allPropertyDataDtos = Database.FetchByGroups(versions, 2000, batch => Sql() - .Select(r => r.Select()) - .From() - .LeftJoin() - .On(left => left.PropertyTypeId, right => right.Id) + .Select() .WhereIn(x => x.VersionId, batch)) - .Where(x => ddefs.Any(y => y.Version == x.VersionId && y.Id == x.NodeId)) // so... probably redundant, but safe .ToList(); + // get PropertyDataDto distinct PropertyTypeDto + var allPropertyTypeIds = allPropertyDataDtos.Select(x => x.PropertyTypeId).Distinct().ToList(); + var allPropertyTypeDtos = Database.FetchByGroups(allPropertyTypeIds, 2000, batch => + Sql() + .Select() + .WhereIn(x => x.Id, batch)); + + // index the types for perfs, and assign to PropertyDataDto + var indexedPropertyTypeDtos = allPropertyTypeDtos.ToDictionary(x => x.Id, x => x); + foreach (var a in allPropertyDataDtos) + a.PropertyTypeDto = indexedPropertyTypeDtos[a.PropertyTypeId]; + // lazy access to prevalue for data types if any property requires tag support var pre = new Lazy>(() => { - var allPropertyTypes = allPropertyData - .Select(x => x.PropertyTypeDto.Id) - .Distinct(); - - var allDataTypePreValue = Database.FetchByGroups(allPropertyTypes, 2000, batch => + return Database.FetchByGroups(allPropertyTypeIds, 2000, batch => Sql() .Select() .From() - .LeftJoin().On(left => left.DataTypeNodeId, right => right.DataTypeId) - .WhereIn(x => x.Id, batch)); - - return allDataTypePreValue; + .WhereIn(x => x.DataTypeNodeId, batch)); }); - return GetPropertyCollection(ddefs, allPropertyData, pre); + // now we have + // - the definitinos + // - all property data dtos + // - a lazy access to prevalues + // and we need to build the proper property collections + + return GetPropertyCollection(temps, allPropertyDataDtos, pre); } - protected IDictionary GetPropertyCollection(DocumentDefinition[] documentDefs, List allPropertyData, Lazy> allPreValues) + private IDictionary GetPropertyCollection(List temps, IEnumerable allPropertyDataDtos, Lazy> allPreValues) { - var result = new Dictionary(); - + var result = new Dictionary(); var propertiesWithTagSupport = new Dictionary(); + var compositionPropertiesIndex = new Dictionary(); - //iterate each definition grouped by it's content type - this will mean less property type iterations while building - // up the property collections - foreach (var compositionGroup in documentDefs.GroupBy(x => x.Composition)) + // index PropertyDataDto per versionId for perfs + var indexedPropertyDataDtos = new Dictionary>(); + foreach (var dto in allPropertyDataDtos) { - var compositionProperties = compositionGroup.Key.CompositionPropertyTypes.ToArray(); + var version = dto.VersionId.Value; + if (indexedPropertyDataDtos.TryGetValue(version, out var list) == false) + indexedPropertyDataDtos[version] = list = new List(); + list.Add(dto); + } - foreach (var def in compositionGroup) + foreach (var temp in temps) + { + // compositionProperties is the property types for the entire composition + // use an index for perfs + if (compositionPropertiesIndex.TryGetValue(temp.Composition.Id, out PropertyType[] compositionProperties) == false) + compositionPropertiesIndex[temp.Composition.Id] = compositionProperties = temp.Composition.CompositionPropertyTypes.ToArray(); + + // map the list of PropertyDataDto to a list of Property + var properties = indexedPropertyDataDtos.TryGetValue(temp.Version, out var propertyDataDtos) + ? PropertyFactory.BuildEntity(propertyDataDtos, compositionProperties, temp.CreateDate, temp.VersionDate).ToList() + : new List(); + + // deal with tags + Dictionary additionalData = null; + foreach (var property in properties) { - var propertyDataDtos = allPropertyData.Where(x => x.NodeId == def.Id).Distinct(); + // test for support and cache + var editor = Current.PropertyEditors[property.PropertyType.PropertyEditorAlias]; + if (propertiesWithTagSupport.TryGetValue(property.PropertyType.PropertyEditorAlias, out SupportTagsAttribute tagSupport) == false) + propertiesWithTagSupport[property.PropertyType.PropertyEditorAlias] = tagSupport = TagExtractor.GetAttribute(editor); + if (tagSupport == null) continue; - var propertyFactory = new PropertyFactory(compositionProperties, def.Version, def.Id, def.CreateDate, def.VersionDate); - var properties = propertyFactory.BuildEntity(propertyDataDtos.ToArray()).ToArray(); + //this property has tags, so we need to extract them and for that we need the prevals which we've already looked up + // fixme - optimize with index + var preValData = allPreValues.Value.Where(x => x.DataTypeNodeId == property.PropertyType.DataTypeDefinitionId) + .Distinct() + .ToArray(); + + // build and set tags + var asDictionary = preValData.ToDictionary(x => x.Alias, x => new PreValue(x.Id, x.Value, x.SortOrder)); + var preVals = new PreValueCollection(asDictionary); + if (additionalData == null) additionalData = new Dictionary(); // reduce allocs + var contentPropData = new ContentPropertyData(property.Value, preVals, additionalData); + TagExtractor.SetPropertyTags(property, contentPropData, property.Value, tagSupport); + } + + if (result.ContainsKey(temp.Version)) + { + var msg = $"The query returned multiple property sets for content {temp.Id}, {temp.Composition.Name}"; + if (VersionableRepositoryBase.ThrowOnWarning) + throw new InvalidOperationException(msg); + Logger.Warn>(msg); + } + + result[temp.Version] = new PropertyCollection(properties); + } + + //// iterate each definition grouped by it's content type, + //// this will mean less property type iterations while building + //// up the property collections + //foreach (var compositionGroup in documentDefs.GroupBy(x => x.Composition)) + //{ + // // compositionGroup.Key is the composition + // // compositionProperties is the property types for the entire composition + // var compositionProperties = compositionGroup.Key.CompositionPropertyTypes.ToArray(); + + // foreach (var def in compositionGroup) + // { + // var properties = indexedPropertyDataDtos.TryGetValue(def.Version, out var propertyDataDtos) + // ? PropertyFactory.BuildEntity(propertyDataDtos, compositionProperties, def.CreateDate, def.VersionDate).ToList() + // : new List(); + + // foreach (var property in properties) + // { + // //NOTE: The benchmarks run with and without the following code show very little change so this is not a perf bottleneck + // var editor = Current.PropertyEditors[property.PropertyType.PropertyEditorAlias]; + + // var tagSupport = propertiesWithTagSupport.ContainsKey(property.PropertyType.PropertyEditorAlias) + // ? propertiesWithTagSupport[property.PropertyType.PropertyEditorAlias] + // : TagExtractor.GetAttribute(editor); + + // if (tagSupport == null) continue; + + // //add to local cache so we don't need to reflect next time for this property editor alias + // propertiesWithTagSupport[property.PropertyType.PropertyEditorAlias] = tagSupport; + + // //this property has tags, so we need to extract them and for that we need the prevals which we've already looked up + // var preValData = allPreValues.Value.Where(x => x.DataTypeNodeId == property.PropertyType.DataTypeDefinitionId) + // .Distinct() + // .ToArray(); + + // var asDictionary = preValData.ToDictionary(x => x.Alias, x => new PreValue(x.Id, x.Value, x.SortOrder)); + // var preVals = new PreValueCollection(asDictionary); + + // var contentPropData = new ContentPropertyData(property.Value, preVals, new Dictionary()); + + // TagExtractor.SetPropertyTags(property, contentPropData, property.Value, tagSupport); + // } + + // if (result.ContainsKey(def.Version)) + // { + // var msg = $"The query returned multiple property sets for document definition {def.Id}, {def.Composition.Name}"; + // if (ThrowOnWarning) + // throw new InvalidOperationException(msg); + // Logger.Warn>(msg); + // } + + // result[def.Version] = new PropertyCollection(properties); + // } + //} + + return result; + } + + // fixme - copied from 7.6... + /* + /// + /// A helper method for inheritors to get the paged results by query in a way that minimizes queries + /// + /// The type of the d. + /// The query. + /// Index of the page. + /// Size of the page. + /// The total records. + /// The tablename + column name for the SELECT statement fragment to return the node id from the query + /// A callback to create the default filter to be applied if there is one + /// A callback to process the query result + /// The order by column + /// The order direction. + /// Flag to indicate when ordering by system field + /// + /// orderBy + protected IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, + Tuple nodeIdSelect, + Func, IEnumerable> processQuery, + string orderBy, + Direction orderDirection, + bool orderBySystemField, + Func> defaultFilter = null) + { + if (orderBy == null) throw new ArgumentNullException(nameof(orderBy)); + + if (query == null) query = QueryT; + var sqlIds = new SqlTranslator(GetBaseQuery(QueryType.Ids), query).Translate(); + var sqlMany = new SqlTranslator(GetBaseQuery(QueryType.Many), query).Translate(); + + // sort and filter + var prepSqlIds = PrepareSqlForPagedResults(sqlIds, filterSql, orderBy, orderDirection, orderBySystemField, table); + //var sqlNodeIdsWithSort = GetSortedSqlForPagedResults( + // GetFilteredSqlForPagedResults(sqlQueryIdsOnly, defaultFilter), + // orderDirection, orderBy, orderBySystemField, nodeIdSelect); + + // get a page of DTOs and the total count + var pagedResult = Database.Page(pageIndex + 1, pageSize, sqlIds); + totalRecords = Convert.ToInt32(pagedResult.TotalItems); + //var pagedResult = Database.Page(pageIndex + 1, pageSize, sqlNodeIdsWithSort); + //totalRecords = Convert.ToInt32(pagedResult.TotalItems); + + + // need to check the actual items returned, not the 'totalRecords', that is because if you request a page number + // that doesn't actually have any data on it, the totalRecords will still indicate there are records but there are none in + // the pageResult, then the GetAll will actually return ALL records in the db. + if (pagedResult.Items.Any() == false) + return Enumerable.Empty(); + + //Create the inner paged query that was used above to get the paged result, we'll use that as the inner sub query + var args = prepSqlIds.Arguments; + Database.BuildPageQueries(pageIndex * pageSize, pageSize, prepSqlIds.SQL, ref args, out string unused, out string sqlPage); + + //We need to make this FULL query an inner join on the paged ID query + var splitQuery = sqlMany.SQL.Split(new[] { "WHERE " }, StringSplitOptions.None); + var fullQueryWithPagedInnerJoin = new Sql(splitQuery[0]) + .Append("INNER JOIN (") + //join the paged query with the paged query arguments + .Append(sqlPage, args) + .Append(") temp ") + .Append(string.Format("ON {0}.{1} = temp.{1}", nodeIdSelect.Item1, nodeIdSelect.Item2)) + //add the original where clause back with the original arguments + .Where(splitQuery[1], sqlIds.Arguments); + + //get sorted and filtered sql + var fullQuery = GetSortedSqlForPagedResults( + GetFilteredSqlForPagedResults(fullQueryWithPagedInnerJoin, defaultFilter), + orderDirection, orderBy, orderBySystemField, nodeIdSelect); + + return processQuery(fullQuery, new PagingSqlQuery(Database, sqlNodeIdsWithSort, pageIndex, pageSize)); + } + + protected IDictionary GetPropertyCollection(Sql sql, IReadOnlyCollection documentDefs) + { + return GetPropertyCollection(new PagingSqlQuery(sql), documentDefs); + } + + protected IDictionary GetPropertyCollection(PagingSqlQuery pagingSqlQuery, IReadOnlyCollection documentDefs) + { + if (documentDefs.Count == 0) return new Dictionary(); + + //initialize to the query passed in + var docSql = pagingSqlQuery.QuerySql; + + //we need to parse the original SQL statement and reduce the columns to just cmsContent.nodeId, cmsContentVersion.VersionId so that we can use + // the statement to go get the property data for all of the items by using an inner join + var parsedOriginalSql = "SELECT {0} " + docSql.SQL.Substring(docSql.SQL.IndexOf("FROM", StringComparison.Ordinal)); + + if (pagingSqlQuery.HasPaging) + { + //if this is a paged query, build the paged query with the custom column substitution, then re-assign + docSql = pagingSqlQuery.BuildPagedQuery("{0}"); + parsedOriginalSql = docSql.SQL; + } + else if (parsedOriginalSql.InvariantContains("ORDER BY ")) + { + //now remove everything from an Orderby clause and beyond if this is unpaged data + parsedOriginalSql = parsedOriginalSql.Substring(0, parsedOriginalSql.LastIndexOf("ORDER BY ", StringComparison.Ordinal)); + } + + //This retrieves all pre-values for all data types that are referenced for all property types + // that exist in the data set. + //Benchmarks show that eagerly loading these so that we can lazily read the property data + // below (with the use of Query intead of Fetch) go about 30% faster, so we'll eagerly load + // this now since we cannot execute another reader inside of reading the property data. + var preValsSql = new Sql(@"SELECT a.id, a.value, a.sortorder, a.alias, a.datatypeNodeId +FROM cmsDataTypePreValues a +WHERE EXISTS( + SELECT DISTINCT b.id as preValIdInner + FROM cmsDataTypePreValues b + INNER JOIN cmsPropertyType + ON b.datatypeNodeId = cmsPropertyType.dataTypeId + INNER JOIN + (" + string.Format(parsedOriginalSql, "cmsContent.contentType") + @") as docData + ON cmsPropertyType.contentTypeId = docData.contentType + WHERE a.id = b.id)", docSql.Arguments); + + var allPreValues = Database.Fetch(preValsSql); + + //It's Important with the sort order here! We require this to be sorted by node id, + // this is required because this data set can be huge depending on the page size. Due + // to it's size we need to be smart about iterating over the property values to build + // the document. Before we used to use Linq to get the property data for a given content node + // and perform a Distinct() call. This kills performance because that would mean if we had 7000 nodes + // and on each iteration we will perform a lookup on potentially 100,000 property rows against the node + // id which turns out to be a crazy amount of iterations. Instead we know it's sorted by this value we'll + // keep an index stored of the rows being read so we never have to re-iterate the entire data set + // on each document iteration. + var propSql = new Sql(@"SELECT cmsPropertyData.* +FROM cmsPropertyData +INNER JOIN cmsPropertyType +ON cmsPropertyData.propertytypeid = cmsPropertyType.id +INNER JOIN + (" + string.Format(parsedOriginalSql, "cmsContent.nodeId, cmsContentVersion.VersionId") + @") as docData +ON cmsPropertyData.versionId = docData.VersionId AND cmsPropertyData.contentNodeId = docData.nodeId +ORDER BY contentNodeId, versionId, propertytypeid +", docSql.Arguments); + + //This does NOT fetch all data into memory in a list, this will read + // over the records as a data reader, this is much better for performance and memory, + // but it means that during the reading of this data set, nothing else can be read + // from SQL server otherwise we'll get an exception. + var allPropertyData = Database.Query(propSql); + + var result = new Dictionary(); + var propertiesWithTagSupport = new Dictionary(); + //used to track the resolved composition property types per content type so we don't have to re-resolve (ToArray) the list every time + var resolvedCompositionProperties = new Dictionary(); + + //keep track of the current property data item being enumerated + var propertyDataSetEnumerator = allPropertyData.GetEnumerator(); + var hasCurrent = false; // initially there is no enumerator.Current + + var comparer = new DocumentDefinitionComparer(SqlSyntax); + + try + { + //This must be sorted by node id because this is how we are sorting the query to lookup property types above, + // which allows us to more efficiently iterate over the large data set of property values + foreach (var def in documentDefs.OrderBy(x => x.Id).ThenBy(x => x.Version, comparer)) + { + // get the resolved properties from our local cache, or resolve them and put them in cache + PropertyType[] compositionProperties; + if (resolvedCompositionProperties.ContainsKey(def.Composition.Id)) + { + compositionProperties = resolvedCompositionProperties[def.Composition.Id]; + } + else + { + compositionProperties = def.Composition.CompositionPropertyTypes.ToArray(); + resolvedCompositionProperties[def.Composition.Id] = compositionProperties; + } + + // assemble the dtos for this def + // use the available enumerator.Current if any else move to next + var propertyDataDtos = new List(); + while (hasCurrent || propertyDataSetEnumerator.MoveNext()) + { + //Not checking null on VersionId because it can never be null - no idea why it's set to nullable + // ReSharper disable once PossibleInvalidOperationException + if (propertyDataSetEnumerator.Current.VersionId.Value == def.Version) + { + hasCurrent = false; // enumerator.Current is not available + propertyDataDtos.Add(propertyDataSetEnumerator.Current); + } + else + { + hasCurrent = true; // enumerator.Current is available for another def + break; // no more propertyDataDto for this def + } + } + + var properties = PropertyFactory.BuildEntity(propertyDataDtos, compositionProperties, def.CreateDate, def.VersionDate).ToArray(); foreach (var property in properties) { @@ -517,7 +819,7 @@ namespace Umbraco.Core.Persistence.Repositories propertiesWithTagSupport[property.PropertyType.PropertyEditorAlias] = tagSupport; //this property has tags, so we need to extract them and for that we need the prevals which we've already looked up - var preValData = allPreValues.Value.Where(x => x.DataTypeNodeId == property.PropertyType.DataTypeDefinitionId) + var preValData = allPreValues.Where(x => x.DataTypeNodeId == property.PropertyType.DataTypeDefinitionId) .Distinct() .ToArray(); @@ -525,45 +827,36 @@ namespace Umbraco.Core.Persistence.Repositories var preVals = new PreValueCollection(asDictionary); - var contentPropData = new ContentPropertyData(property.Value, - preVals, - new Dictionary()); + var contentPropData = new ContentPropertyData(property.Value, preVals, new Dictionary()); TagExtractor.SetPropertyTags(property, contentPropData, property.Value, tagSupport); } } - if (result.ContainsKey(def.Id)) + if (result.ContainsKey(def.Version)) { - Logger.Warn>("The query returned multiple property sets for document definition " + def.Id + ", " + def.Composition.Name); + var msg = string.Format("The query returned multiple property sets for document definition {0}, {1}, {2}", def.Id, def.Version, def.Composition.Name); + if (ThrowOnWarning) + { + throw new InvalidOperationException(msg); + } + else + { + Logger.Warn>(msg); + } } - result[def.Id] = new PropertyCollection(properties); + result[def.Version] = new PropertyCollection(properties); } } + finally + { + propertyDataSetEnumerator.Dispose(); + } return result; - } - public class DocumentDefinition - { - /// - /// Initializes a new instance of the class. - /// - public DocumentDefinition(int id, Guid version, DateTime versionDate, DateTime createDate, IContentTypeComposition composition) - { - Id = id; - Version = version; - VersionDate = versionDate; - CreateDate = createDate; - Composition = composition; - } - - public int Id { get; set; } - public Guid Version { get; set; } - public DateTime VersionDate { get; set; } - public DateTime CreateDate { get; set; } - public IContentTypeComposition Composition { get; set; } } + */ protected virtual string GetDatabaseFieldNameForOrderBy(string orderBy) { @@ -610,7 +903,6 @@ namespace Umbraco.Core.Persistence.Repositories } public IDatabaseUnitOfWork UnitOfWork { get; } - public TEntity Entity { get; } } @@ -623,10 +915,8 @@ namespace Umbraco.Core.Persistence.Repositories VersionId = versionId; } - public IDatabaseUnitOfWork UnitOfWork { get; private set; } - + public IDatabaseUnitOfWork UnitOfWork { get; } public int EntityId { get; } - public Guid VersionId { get; } } @@ -636,19 +926,256 @@ namespace Umbraco.Core.Persistence.Repositories protected void OnUowRefreshedEntity(UnitOfWorkEntityEventArgs args) { - UowRefreshedEntity.RaiseEvent(args, Instance); + UowRefreshedEntity.RaiseEvent(args, This); } protected void OnUowRemovingEntity(UnitOfWorkEntityEventArgs args) { - UowRemovingEntity.RaiseEvent(args, Instance); + UowRemovingEntity.RaiseEvent(args, This); } protected void OnUowRemovingVersion(UnitOfWorkVersionEventArgs args) { - UowRemovingVersion.RaiseEvent(args, Instance); + UowRemovingVersion.RaiseEvent(args, This); } #endregion + + #region Classes + + protected class TempContent + { + public TempContent(int id, Guid version, DateTime versionDate, DateTime createDate, IContentTypeComposition composition, IContentBase content = null) + { + Id = id; + Version = version; + VersionDate = versionDate; + CreateDate = createDate; + Composition = composition; + Content = content; + } + + public int Id { get; set; } + public Guid Version { get; set; } + public DateTime VersionDate { get; set; } + public DateTime CreateDate { get; set; } + public IContentTypeComposition Composition { get; set; } + + public IContentBase Content { get; set; } + + public int? TemplateId { get; set; } + } + + // fixme copied from 7.6 + + /// + /// For Paging, repositories must support returning different query for the query type specified + /// + /// + /// + protected abstract Sql GetBaseQuery(QueryType queryType); + + /* + internal class DocumentDefinitionCollection : KeyedCollection + { + private readonly bool _includeAllVersions; + + /// + /// Constructor specifying if all versions should be allowed, in that case the key for the collection becomes the versionId (GUID) + /// + /// + public DocumentDefinitionCollection(bool includeAllVersions = false) + { + _includeAllVersions = includeAllVersions; + } + + protected override ValueType GetKeyForItem(DocumentDefinition item) + { + return _includeAllVersions ? (ValueType)item.Version : item.Id; + } + + /// + /// if this key already exists if it does then we need to check + /// if the existing item is 'older' than the new item and if that is the case we'll replace the older one + /// + /// + /// + public bool AddOrUpdate(DocumentDefinition item) + { + //if we are including all versions then just add, we aren't checking for latest + if (_includeAllVersions) + { + Add(item); + return true; + } + + if (Dictionary == null) + { + Add(item); + return true; + } + + var key = GetKeyForItem(item); + if (TryGetValue(key, out DocumentDefinition found)) + { + //it already exists and it's older so we need to replace it + if (item.VersionId <= found.VersionId) return false; + + var currIndex = Items.IndexOf(found); + if (currIndex == -1) + throw new IndexOutOfRangeException("Could not find the item in the list: " + found.Version); + + //replace the current one with the newer one + SetItem(currIndex, item); + return true; + } + + Add(item); + return true; + } + + public bool TryGetValue(ValueType key, out DocumentDefinition val) + { + if (Dictionary != null) + return Dictionary.TryGetValue(key, out val); + + val = null; + return false; + } + } + + /// + /// Implements a Guid comparer that respect the Sql engine ordering. + /// + /// + /// MySql sorts Guids as strings, but MSSQL sorts guids based on a weird byte sections order + /// This comparer compares Guids using the corresponding Sql syntax method, ie the method of the underlying Sql engine. + /// see http://stackoverflow.com/questions/7810602/sql-server-guid-sort-algorithm-why + /// see https://blogs.msdn.microsoft.com/sqlprogrammability/2006/11/06/how-are-guids-compared-in-sql-server-2005/ + /// + private class DocumentDefinitionComparer : IComparer + { + private readonly bool _mySql; + + public DocumentDefinitionComparer(ISqlSyntaxProvider sqlSyntax) + { + _mySql = sqlSyntax is MySqlSyntaxProvider; + } + + public int Compare(Guid x, Guid y) + { + // MySql sorts Guids as string (ie normal, same as .NET) whereas MSSQL + // sorts them on a weird byte sections order + return _mySql ? x.CompareTo(y) : new SqlGuid(x).CompareTo(new SqlGuid(y)); + } + } + + internal class DocumentDefinition + { + /// + /// Initializes a new instance of the class. + /// + public DocumentDefinition(DocumentDto dto, IContentTypeComposition composition) + { + DocumentDto = dto; + ContentVersionDto = dto.ContentVersionDto; + Composition = composition; + } + + public DocumentDefinition(ContentVersionDto dto, IContentTypeComposition composition) + { + ContentVersionDto = dto; + Composition = composition; + } + + public DocumentDto DocumentDto { get; } + public ContentVersionDto ContentVersionDto { get; } + + public int Id => ContentVersionDto.NodeId; + + public Guid Version => DocumentDto?.VersionId ?? ContentVersionDto.VersionId; + + // This is used to determien which version is the most recent + public int VersionId => ContentVersionDto.Id; + + public DateTime VersionDate => ContentVersionDto.VersionDate; + + public DateTime CreateDate => ContentVersionDto.ContentDto.NodeDto.CreateDate; + + public IContentTypeComposition Composition { get; set; } + } + + // Represents a query that may contain paging information. + internal class PagingSqlQuery + { + // the original query sql + public Sql QuerySql { get; } + + public PagingSqlQuery(Sql querySql) + { + QuerySql = querySql; + } + + protected PagingSqlQuery(Sql querySql, int pageSize) + : this(querySql) + { + HasPaging = pageSize > 0; + } + + // whether the paging query is actually paging + public bool HasPaging { get; } + + // the paging sql + public virtual Sql BuildPagedQuery(string columns) + { + throw new InvalidOperationException("This query has no paging information."); + } + } + + /// + /// Represents a query that may contain paging information. + /// + /// + internal class PagingSqlQuery : PagingSqlQuery // fixme what's here? + { + private readonly Database _db; + private readonly long _pageIndex; + private readonly int _pageSize; + + // fixme - don't capture a db instance here! + // instead, should have an extension method, so one can do + // sql = db.BuildPageQuery(pagingQuery, columns) + public PagingSqlQuery(Database db, Sql querySql, long pageIndex, int pageSize) + : base(querySql, pageSize) + { + _db = db; + _pageIndex = pageIndex; + _pageSize = pageSize; + } + + /// + /// Creates a paged query based on the original query and subtitutes the selectColumns specified + /// + /// + /// + // build a page query + public override Sql BuildPagedQuery(string columns) + { + if (HasPaging == false) + throw new InvalidOperationException("This query has no paging information."); + + // substitutes the original "SELECT ..." with "SELECT {columns}" ie only + // select the specified columns - fixme why? + var sql = $"SELECT {columns} {QuerySql.SQL.Substring(QuerySql.SQL.IndexOf("FROM", StringComparison.Ordinal))}"; + + // and then build the page query + var args = QuerySql.Arguments; + _db.BuildPageQueries(_pageIndex * _pageSize, _pageSize, sql, ref args, out string unused, out string sqlPage); + return new Sql(sqlPage, args); + } + } + */ + + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBaseAliasRegex.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBaseAliasRegex.cs new file mode 100644 index 0000000000..13933dfc34 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBaseAliasRegex.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Repositories +{ + internal static class VersionableRepositoryBaseAliasRegex + { + private static readonly Dictionary Regexes = new Dictionary(); + + public static Regex For(ISqlSyntaxProvider sqlSyntax) + { + var type = sqlSyntax.GetType(); + Regex aliasRegex; + if (Regexes.TryGetValue(type, out aliasRegex)) + return aliasRegex; + + var col = Regex.Escape(sqlSyntax.GetQuotedColumnName("column")).Replace("column", @"\w+"); + var fld = Regex.Escape(sqlSyntax.GetQuotedTableName("table") + ".").Replace("table", @"\w+") + col; + aliasRegex = new Regex("(" + fld + @")\s+AS\s+(" + col + ")", RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.IgnoreCase); + Regexes[type] = aliasRegex; + return aliasRegex; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/RepositoryFactory.cs b/src/Umbraco.Core/Persistence/RepositoryFactory.cs index 05a33676bc..cfc629a80d 100644 --- a/src/Umbraco.Core/Persistence/RepositoryFactory.cs +++ b/src/Umbraco.Core/Persistence/RepositoryFactory.cs @@ -18,8 +18,7 @@ namespace Umbraco.Core.Persistence /// A container. public RepositoryFactory(IServiceContainer container) { - if (container == null) throw new ArgumentNullException(nameof(container)); - _container = container; + _container = container ?? throw new ArgumentNullException(nameof(container)); } /// @@ -29,27 +28,12 @@ namespace Umbraco.Core.Persistence /// A unit of work. /// The optional name of the repository. /// The created repository for the unit of work. - public virtual TRepository CreateRepository(IDatabaseUnitOfWork uow, string name = null) + public virtual TRepository CreateRepository(IScopeUnitOfWork uow, string name = null) where TRepository : IRepository { return string.IsNullOrWhiteSpace(name) - ? _container.GetInstance(uow) - : _container.GetInstance(uow, name); - } - - /// - /// Creates a repository. - /// - /// The type of the repository. - /// A unit of work. - /// The optional name of the repository. - /// The created repository for the unit of work. - internal virtual TRepository CreateRepository(FileUnitOfWork uow, string name = null) - where TRepository : IRepository - { - return string.IsNullOrWhiteSpace(name) - ? _container.GetInstance(uow) - : _container.GetInstance(uow, name); + ? _container.GetInstance(uow) + : _container.GetInstance(uow, name); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs index 00e0a43f81..e1172b6497 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using NPoco; using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Scoping; namespace Umbraco.Core.Persistence.SqlSyntax { @@ -13,14 +14,19 @@ namespace Umbraco.Core.Persistence.SqlSyntax public class SqlServerSyntaxProvider : MicrosoftSqlSyntaxProviderBase { // IUmbracoDatabaseFactory to be lazily injected - public SqlServerSyntaxProvider(Lazy lazyFactory) + public SqlServerSyntaxProvider(Lazy lazyScopeProvider) { _serverVersion = new Lazy(() => { - var factory = lazyFactory.Value; - if (factory == null) - throw new InvalidOperationException("Failed to determine Sql Server version (no database factory)."); - return DetermineVersion(factory); + var scopeProvider = lazyScopeProvider.Value; + if (scopeProvider == null) + throw new InvalidOperationException("Failed to determine Sql Server version (no scope provider)."); + using (var scope = scopeProvider.CreateScope()) + { + var version = DetermineVersion(scope.Database); + scope.Complete(); + return version; + } }); } @@ -99,7 +105,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax } } - private static ServerVersionInfo DetermineVersion(IUmbracoDatabaseFactory factory) + private static ServerVersionInfo DetermineVersion(IUmbracoDatabase database) { // Edition: "Express Edition", "Windows Azure SQL Database..." // EngineEdition: 1/Desktop 2/Standard 3/Enterprise 4/Express 5/Azure @@ -119,7 +125,6 @@ namespace Umbraco.Core.Persistence.SqlSyntax try { - var database = factory.GetDatabase(); var version = database.Fetch(sql).First(); version.Initialize(); return version; diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs index 5cfca7cb3d..e99b4ed826 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs @@ -1,9 +1,24 @@ using NPoco; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Core.Persistence.SqlSyntax { internal static class SqlSyntaxProviderExtensions { + public static IEnumerable GetDefinedIndexesDefinitions(this ISqlSyntaxProvider sql, IDatabase db) + { + return sql.GetDefinedIndexes(db) + .Select(x => new DbIndexDefinition + { + TableName = x.Item1, + IndexName = x.Item2, + ColumnName = x.Item3, + IsUnique = x.Item4 + }).ToArray(); + } + /// /// Returns the quotes tableName.columnName combo /// diff --git a/src/Umbraco.Core/Persistence/ThreadStaticDatabaseScopeAccessor.cs b/src/Umbraco.Core/Persistence/ThreadStaticDatabaseScopeAccessor.cs index b002dbb4f9..7e09341412 100644 --- a/src/Umbraco.Core/Persistence/ThreadStaticDatabaseScopeAccessor.cs +++ b/src/Umbraco.Core/Persistence/ThreadStaticDatabaseScopeAccessor.cs @@ -1,18 +1,18 @@ -using System; +// fixme - remove this file +//using System; -namespace Umbraco.Core.Persistence -{ - // fixme - move this to test or whatever! +//namespace Umbraco.Core.Persistence +//{ - internal class ThreadStaticDatabaseScopeAccessor : IDatabaseScopeAccessor - { - [ThreadStatic] - private static DatabaseScope _databaseScope; +// internal class ThreadStaticDatabaseScopeAccessor : IDatabaseScopeAccessor +// { +// [ThreadStatic] +// private static DatabaseScope _databaseScope; - public DatabaseScope Scope - { - get { return _databaseScope; } - set { _databaseScope = value; } - } - } -} +// public DatabaseScope Scope +// { +// get { return _databaseScope; } +// set { _databaseScope = value; } +// } +// } +//} diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs index ab43b2d9f5..b6f3d7d8a7 100644 --- a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs @@ -102,6 +102,27 @@ namespace Umbraco.Core.Persistence } } + /// + public bool InTransaction { get; private set; } + + 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. /// @@ -137,6 +158,7 @@ namespace Umbraco.Core.Persistence if (connection == null) throw new ArgumentNullException(nameof(connection)); #if DEBUG_DATABASES + // determines the database connection SPID for debugging if (DatabaseType == DBType.MySql) { using (var command = connection.CreateCommand()) diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs b/src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs index c252ce2608..711ae5fcca 100644 --- a/src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs +++ b/src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs @@ -28,7 +28,6 @@ namespace Umbraco.Core.Persistence /// internal class UmbracoDatabaseFactory : DisposableObject, IUmbracoDatabaseFactory { - private readonly IDatabaseScopeAccessor _databaseScopeAccessor; private readonly ISqlSyntaxProvider[] _sqlSyntaxProviders; private readonly IMapperCollection _mappers; private readonly ILogger _logger; @@ -51,8 +50,8 @@ namespace Umbraco.Core.Persistence /// Initializes a new instance of the . /// /// Used by LightInject. - public UmbracoDatabaseFactory(IEnumerable sqlSyntaxProviders, ILogger logger, IDatabaseScopeAccessor databaseScopeAccessor, IMapperCollection mappers) - : this(GlobalSettings.UmbracoConnectionName, sqlSyntaxProviders, logger, databaseScopeAccessor, mappers) + public UmbracoDatabaseFactory(IEnumerable sqlSyntaxProviders, ILogger logger, IMapperCollection mappers) + : this(Constants.System.UmbracoConnectionName, sqlSyntaxProviders, logger, mappers) { if (Configured == false) DatabaseBuilder.GiveLegacyAChance(this, logger); @@ -62,18 +61,13 @@ namespace Umbraco.Core.Persistence /// Initializes a new instance of the . /// /// Used by the other ctor and in tests. - public UmbracoDatabaseFactory(string connectionStringName, IEnumerable sqlSyntaxProviders, ILogger logger, IDatabaseScopeAccessor databaseScopeAccessor, IMapperCollection mappers) + public UmbracoDatabaseFactory(string connectionStringName, IEnumerable sqlSyntaxProviders, ILogger logger, IMapperCollection mappers) { - if (sqlSyntaxProviders == null) throw new ArgumentNullException(nameof(sqlSyntaxProviders)); - if (logger == null) throw new ArgumentNullException(nameof(logger)); - if (databaseScopeAccessor == null) throw new ArgumentNullException(nameof(databaseScopeAccessor)); if (string.IsNullOrWhiteSpace(connectionStringName)) throw new ArgumentNullOrEmptyException(nameof(connectionStringName)); - if (mappers == null) throw new ArgumentNullException(nameof(mappers)); - _mappers = mappers; - _sqlSyntaxProviders = sqlSyntaxProviders.ToArray(); - _logger = logger; - _databaseScopeAccessor = databaseScopeAccessor; + _mappers = mappers ?? throw new ArgumentNullException(nameof(mappers)); + _sqlSyntaxProviders = sqlSyntaxProviders?.ToArray() ?? throw new ArgumentNullException(nameof(sqlSyntaxProviders)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); var settings = ConfigurationManager.ConnectionStrings[connectionStringName]; if (settings == null) @@ -96,17 +90,11 @@ namespace Umbraco.Core.Persistence /// Initializes a new instance of the . /// /// Used in tests. - public UmbracoDatabaseFactory(string connectionString, string providerName, IEnumerable sqlSyntaxProviders, ILogger logger, IDatabaseScopeAccessor databaseScopeAccessor, IMapperCollection mappers) + public UmbracoDatabaseFactory(string connectionString, string providerName, IEnumerable sqlSyntaxProviders, ILogger logger, IMapperCollection mappers) { - if (sqlSyntaxProviders == null) throw new ArgumentNullException(nameof(sqlSyntaxProviders)); - if (logger == null) throw new ArgumentNullException(nameof(logger)); - if (databaseScopeAccessor == null) throw new ArgumentNullException(nameof(databaseScopeAccessor)); - if (mappers == null) throw new ArgumentNullException(nameof(mappers)); - - _mappers = mappers; - _sqlSyntaxProviders = sqlSyntaxProviders.ToArray(); - _logger = logger; - _databaseScopeAccessor = databaseScopeAccessor; + _mappers = mappers ?? throw new ArgumentNullException(nameof(mappers)); + _sqlSyntaxProviders = sqlSyntaxProviders?.ToArray() ?? throw new ArgumentNullException(nameof(sqlSyntaxProviders)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); if (string.IsNullOrWhiteSpace(connectionString) || string.IsNullOrWhiteSpace(providerName)) { @@ -211,31 +199,12 @@ namespace Umbraco.Core.Persistence } } - /// - public IUmbracoDatabase Database => GetDatabase(); - - /// - public IUmbracoDatabase GetDatabase() - { - EnsureConfigured(); - - var scope = _databaseScopeAccessor.Scope; - if (scope == null) throw new InvalidOperationException("Out of scope."); - return scope.Database; - } - /// public IUmbracoDatabase CreateDatabase() { return (IUmbracoDatabase) _npocoDatabaseFactory.GetDatabase(); } - /// - public IDatabaseScope CreateScope(IUmbracoDatabase database = null) - { - return new DatabaseScope(_databaseScopeAccessor, this, database); - } - // gets the sql syntax provider that corresponds, from attribute private ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName) { @@ -289,7 +258,7 @@ namespace Umbraco.Core.Persistence //var db = _umbracoDatabaseAccessor.UmbracoDatabase; //_umbracoDatabaseAccessor.UmbracoDatabase = null; //db?.Dispose(); - _databaseScopeAccessor.Scope = null; + //_databaseScopeAccessor.Scope = null; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/FileUnitOfWork.cs b/src/Umbraco.Core/Persistence/UnitOfWork/FileUnitOfWork.cs deleted file mode 100644 index f7655a0cf9..0000000000 --- a/src/Umbraco.Core/Persistence/UnitOfWork/FileUnitOfWork.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Umbraco.Core.Persistence.UnitOfWork -{ - /// - /// Represents a persistence unit of work for working with files. - /// - /// The FileUnitOfWork does *not* implement transactions, so although it needs to be flushed or - /// completed for operations to be executed, they are executed immediately without any commit or roll back - /// mechanism. - internal class FileUnitOfWork : UnitOfWorkBase - { - /// - /// Initializes a new instance of the class with a a repository factory. - /// - /// A repository factory. - /// This should be used by the FileUnitOfWorkProvider exclusively. - public FileUnitOfWork(RepositoryFactory repositoryFactory) - : base(repositoryFactory) - { } - - /// - /// Creates a repository. - /// - /// The type of the repository. - /// The optional name of the repository. - /// The created repository for the unit of work. - public override TRepository CreateRepository(string name = null) - { - return RepositoryFactory.CreateRepository(this, name); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/FileUnitOfWorkProvider.cs b/src/Umbraco.Core/Persistence/UnitOfWork/FileUnitOfWorkProvider.cs deleted file mode 100644 index 52f0b6cbe0..0000000000 --- a/src/Umbraco.Core/Persistence/UnitOfWork/FileUnitOfWorkProvider.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace Umbraco.Core.Persistence.UnitOfWork -{ - /// - /// Represents a Unit of Work Provider for creating a - /// - public class FileUnitOfWorkProvider : IUnitOfWorkProvider - { - private readonly RepositoryFactory _repositoryFactory; - - /// - /// Initializes a new instance of the class with a repository factory. - /// - /// A repository factory. - public FileUnitOfWorkProvider(RepositoryFactory repositoryFactory) - { - Mandate.ParameterNotNull(repositoryFactory, nameof(repositoryFactory)); - _repositoryFactory = repositoryFactory; - } - - /// - /// Initializes a new instance of the class. - /// - /// FOR UNIT TESTS ONLY - internal FileUnitOfWorkProvider() - { - // careful, _repositoryFactory remains null! - } - - public IUnitOfWork CreateUnitOfWork() - { - return new FileUnitOfWork(_repositoryFactory); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/IDatabaseUnitOfWorkProvider.cs b/src/Umbraco.Core/Persistence/UnitOfWork/IDatabaseUnitOfWorkProvider.cs index 1385d7fe53..2c1a261faa 100644 --- a/src/Umbraco.Core/Persistence/UnitOfWork/IDatabaseUnitOfWorkProvider.cs +++ b/src/Umbraco.Core/Persistence/UnitOfWork/IDatabaseUnitOfWorkProvider.cs @@ -5,11 +5,6 @@ namespace Umbraco.Core.Persistence.UnitOfWork /// public interface IDatabaseUnitOfWorkProvider { - /// - /// Gets the database factory. - /// - IUmbracoDatabaseFactory DatabaseFactory { get; } - /// /// Creates a unit of work. /// diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/IScopeUnitOfWork.cs b/src/Umbraco.Core/Persistence/UnitOfWork/IScopeUnitOfWork.cs new file mode 100644 index 0000000000..e5232092c5 --- /dev/null +++ b/src/Umbraco.Core/Persistence/UnitOfWork/IScopeUnitOfWork.cs @@ -0,0 +1,12 @@ +using Umbraco.Core.Events; +using Umbraco.Core.Scoping; + +namespace Umbraco.Core.Persistence.UnitOfWork +{ + public interface IScopeUnitOfWork : IDatabaseUnitOfWork + { + IScope Scope { get; } + EventMessages Messages { get; } + IEventDispatcher Events { get; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/IScopeUnitOfWorkProvider.cs b/src/Umbraco.Core/Persistence/UnitOfWork/IScopeUnitOfWorkProvider.cs new file mode 100644 index 0000000000..76e6cdd798 --- /dev/null +++ b/src/Umbraco.Core/Persistence/UnitOfWork/IScopeUnitOfWorkProvider.cs @@ -0,0 +1,29 @@ +using System.Data; +using Umbraco.Core.Scoping; + +namespace Umbraco.Core.Persistence.UnitOfWork +{ + /// + /// Provides scoped units of work. + /// + public interface IScopeUnitOfWorkProvider : IDatabaseUnitOfWorkProvider + { + // gets the scope provider + IScopeProvider ScopeProvider { get; } + + // creates a unit of work + // redefine the method to indicate it returns an IScopeUnitOfWork and + // not anymore only an IDatabaseUnitOfWork as IDatabaseUnitOfWorkProvider does + new IScopeUnitOfWork CreateUnitOfWork(); + + // creates a unit of work + // support specifying an isolation level + // support auto-commit - but beware! it will be committed, whatever happens + // fixme in v8 this should all be merged as one single method with optional args + IScopeUnitOfWork CreateUnitOfWork(bool readOnly); + IScopeUnitOfWork CreateUnitOfWork(IsolationLevel isolationLevel, bool readOnly = false); + + // fixme explain + IDatabaseContext DatabaseContext { get; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/IUnitOfWork.cs b/src/Umbraco.Core/Persistence/UnitOfWork/IUnitOfWork.cs index e59473436e..93cafcb493 100644 --- a/src/Umbraco.Core/Persistence/UnitOfWork/IUnitOfWork.cs +++ b/src/Umbraco.Core/Persistence/UnitOfWork/IUnitOfWork.cs @@ -30,6 +30,9 @@ namespace Umbraco.Core.Persistence.UnitOfWork /// The repository in charge of the entity. void RegisterDeleted(IEntity entity, IUnitOfWorkRepository repository); + // fixme - we should get rid of all references to database here, or merge IUnitOfWork with IDatabaseUnitOfWork + // fixme - do we have a scope.Begin()? or is it even automatic? and then do we need Begin() at all? + /// /// Begins the unit of work. /// @@ -75,6 +78,7 @@ namespace Umbraco.Core.Persistence.UnitOfWork /// The type of the repository. /// The optional name of the repository. /// The created repository for the unit of work. - TRepository CreateRepository(string name = null) where TRepository : IRepository; + TRepository CreateRepository(string name = null) + where TRepository : IRepository; } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/NPocoUnitOfWork.cs b/src/Umbraco.Core/Persistence/UnitOfWork/NPocoUnitOfWork.cs deleted file mode 100644 index 329c8cc09b..0000000000 --- a/src/Umbraco.Core/Persistence/UnitOfWork/NPocoUnitOfWork.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Data; -using NPoco; -using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Persistence.SqlSyntax; - -namespace Umbraco.Core.Persistence.UnitOfWork -{ - /// - /// Implements IDatabaseUnitOfWork for NPoco. - /// - internal class NPocoUnitOfWork : UnitOfWorkBase, IDatabaseUnitOfWork - { - private ITransaction _transaction; - - /// - /// Initializes a new instance of the class with a database and a repository factory. - /// - /// The database. - /// A repository factory. - /// This should be used by the NPocoUnitOfWorkProvider exclusively. - internal NPocoUnitOfWork(IUmbracoDatabase database, RepositoryFactory repositoryFactory) - : base(repositoryFactory) - { - Database = database; - } - - /// - public IUmbracoDatabase Database { get; } - - #region IDatabaseContext - - /// - public ISqlSyntaxProvider SqlSyntax => Database.SqlSyntax; - - /// - public Sql Sql() => new Sql(Database.SqlContext); - - /// - public Sql Sql(string sql, params object[] args) => new Sql(Database.SqlContext, sql, args); - - /// - public IQuery Query() => new Query(Database.SqlContext); - - #endregion - - /// - public override TRepository CreateRepository(string name = null) - { - return RepositoryFactory.CreateRepository(this, name); - } - - /// - public override void Begin() - { - base.Begin(); - - if (_transaction == null) - _transaction = Database.GetTransaction(); - } - - /// - public void ReadLock(params int[] lockIds) - { - Begin(); // we need a transaction - - if (Database.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) - throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); - // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks - foreach (var lockId in lockIds) - { - var i = Database.ExecuteScalar("SELECT value FROM umbracoLock WHERE id=@id", new { id = lockId }); - if (i == null) // ensure we are actually locking! - throw new Exception($"LockObject with id={lockId} does not exist."); - } - } - - /// - public void WriteLock(params int[] lockIds) - { - Begin(); // we need a transaction - - if (Database.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) - throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); - // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks - foreach (var lockId in lockIds) - { - var i = Database.Execute("UPDATE umbracoLock SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { id = lockId }); - if (i == 0) // ensure we are actually locking! - throw new Exception($"LockObject with id={lockId} does not exist."); - } - } - - /// - protected override void DisposeResources() - { - base.DisposeResources(); - - // no transaction, nothing to do - if (_transaction == null) return; - - // will either complete or abort NPoco transaction - // which means going one level up in the transaction stack - // and commit or rollback only if at top of stack - if (Completed) - _transaction.Complete(); // complete the transaction - else - _transaction.Dispose(); // abort the transaction - fixme or should we always dispose it?! - - _transaction = null; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/NPocoUnitOfWorkProvider.cs b/src/Umbraco.Core/Persistence/UnitOfWork/NPocoUnitOfWorkProvider.cs deleted file mode 100644 index b852855489..0000000000 --- a/src/Umbraco.Core/Persistence/UnitOfWork/NPocoUnitOfWorkProvider.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; - -namespace Umbraco.Core.Persistence.UnitOfWork -{ - /// - /// Represents a provider that creates instances. - /// - public class NPocoUnitOfWorkProvider : IDatabaseUnitOfWorkProvider - { - private readonly RepositoryFactory _repositoryFactory; - - /// - /// Initializes a new instance of the class with a database factory and a repository factory. - /// - /// A database factory. - /// A repository factory. - public NPocoUnitOfWorkProvider(IUmbracoDatabaseFactory databaseFactory, RepositoryFactory repositoryFactory) - { - if (databaseFactory == null) throw new ArgumentNullException(nameof(databaseFactory)); - if (repositoryFactory == null) throw new ArgumentNullException(nameof(repositoryFactory)); - - DatabaseFactory = databaseFactory; - _repositoryFactory = repositoryFactory; - } - - /// - public IUmbracoDatabaseFactory DatabaseFactory { get; } - - /// - public IDatabaseUnitOfWork CreateUnitOfWork() - { - return new NPocoUnitOfWork(DatabaseFactory.Database, _repositoryFactory); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/ScopeUnitOfWork.cs b/src/Umbraco.Core/Persistence/UnitOfWork/ScopeUnitOfWork.cs new file mode 100644 index 0000000000..de737d70c3 --- /dev/null +++ b/src/Umbraco.Core/Persistence/UnitOfWork/ScopeUnitOfWork.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Data; +using NPoco; +using Umbraco.Core.Events; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Scoping; + +namespace Umbraco.Core.Persistence.UnitOfWork +{ + /// + /// Represents a scoped unit of work. + /// + internal class ScopeUnitOfWork : UnitOfWorkBase, IScopeUnitOfWork + { + private readonly IsolationLevel _isolationLevel; + private readonly IScopeProvider _scopeProvider; + private bool _completeScope; + private IScope _scope; + private Guid _key; + + /// + /// Used for testing + /// + internal Guid InstanceId { get; } + + /// + /// Creates a new unit of work instance + /// + /// + /// + /// + /// + /// + /// This should normally not be used directly and should be created with the UnitOfWorkProvider + /// + internal ScopeUnitOfWork(IScopeProvider scopeProvider, RepositoryFactory repositoryFactory, IsolationLevel isolationLevel = IsolationLevel.Unspecified, bool readOnly = false) + : base(repositoryFactory, readOnly) + { + _scopeProvider = scopeProvider; + _isolationLevel = isolationLevel; + + // fixme only 1! + _key = Guid.NewGuid(); + InstanceId = Guid.NewGuid(); + + // be false by default + // if set to true... the UnitOfWork is "auto-commit" which means that even in the case of + // an exception, the scope would still be completed - ppl should use it with great care! + _completeScope = readOnly; + } + + #region IDatabaseContext + + /// + public ISqlSyntaxProvider SqlSyntax => Database.SqlSyntax; + + /// + public Sql Sql() => new Sql(Database.SqlContext); + + /// + public Sql Sql(string sql, params object[] args) => new Sql(Database.SqlContext, sql, args); + + /// + public IQuery Query() => new Query(Database.SqlContext); + + #endregion + + public override TRepository CreateRepository(string name = null) + { + return RepositoryFactory.CreateRepository(this, name); + } + + public override void Begin() + { + base.Begin(); + + // soon as we get Database, a transaction is started + var unused = Database; + } + + /// + public void ReadLock(params int[] lockIds) + { + // soon as we get Database, a transaction is started + + if (Database.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) + throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); + + // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks + foreach (var lockId in lockIds) + { + var i = Database.ExecuteScalar("SELECT value FROM umbracoLock WHERE id=@id", new { id = lockId }); + if (i == null) // ensure we are actually locking! + throw new Exception($"LockObject with id={lockId} does not exist."); + } + } + + /// + public void WriteLock(params int[] lockIds) + { + if (ReadOnly) + throw new NotSupportedException("This unit of work is read-only."); + + // soon as we get Database, a transaction is started + + if (Database.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) + throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); + + // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks + foreach (var lockId in lockIds) + { + var i = Database.Execute("UPDATE umbracoLock SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { id = lockId }); + if (i == 0) // ensure we are actually locking! + throw new Exception($"LockObject with id={lockId} does not exist."); + } + } + + public override void Complete() + { + base.Complete(); + _completeScope = true; + _key = Guid.NewGuid(); // fixme kill! + } + + public object Key => _key; + + // fixme v8 + // once we are absolutely sure that our UOW cannot be disposed more than once, + // this should throw if the UOW has already been disposed, NOT recreate a scope! + public IScope Scope => _scope ?? (_scope = _scopeProvider.CreateScope(_isolationLevel)); + + public IUmbracoDatabase Database => Scope.Database; + + public EventMessages Messages => Scope.Messages; + + public IEventDispatcher Events => Scope.Events; + + /// + /// Ensures disposable objects are disposed + /// + /// + /// Ensures that the Transaction instance is disposed of + /// + protected override void DisposeResources() + { + // base deals with the operation's queue + base.DisposeResources(); + + if (_scope == null) return; + if (_completeScope) _scope.Complete(); + _scope.Dispose(); + _scope = null; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/ScopeUnitOfWorkProvider.cs b/src/Umbraco.Core/Persistence/UnitOfWork/ScopeUnitOfWorkProvider.cs new file mode 100644 index 0000000000..8a7c6971d4 --- /dev/null +++ b/src/Umbraco.Core/Persistence/UnitOfWork/ScopeUnitOfWorkProvider.cs @@ -0,0 +1,57 @@ +using System; +using System.Data; +using Umbraco.Core.Scoping; + +namespace Umbraco.Core.Persistence.UnitOfWork +{ + public class ScopeUnitOfWorkProvider : IScopeUnitOfWorkProvider + { + private readonly RepositoryFactory _repositoryFactory; + + /// + /// Initializes a new instance of the class. + /// + public ScopeUnitOfWorkProvider(IScopeProvider scopeProvider, IDatabaseContext databaseContext, RepositoryFactory repositoryFactory) + { + ScopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider)); + DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext)); + _repositoryFactory = repositoryFactory ?? throw new ArgumentNullException(nameof(repositoryFactory)); + } + + /// + public IScopeProvider ScopeProvider { get; } + + /// + public IDatabaseContext DatabaseContext { get; } + + // explicit implementation + IDatabaseUnitOfWork IDatabaseUnitOfWorkProvider.CreateUnitOfWork() + { + return new ScopeUnitOfWork(ScopeProvider, _repositoryFactory); + } + + /// + public virtual IScopeUnitOfWork CreateUnitOfWork() + { + return new ScopeUnitOfWork(ScopeProvider, _repositoryFactory); + } + + /// + public IScopeUnitOfWork CreateUnitOfWork(IsolationLevel isolationLevel) + { + return new ScopeUnitOfWork(ScopeProvider, _repositoryFactory, isolationLevel); + } + + /// + public IScopeUnitOfWork CreateUnitOfWork(bool readOnly) + { + return new ScopeUnitOfWork(ScopeProvider, _repositoryFactory, readOnly: readOnly); + } + + /// + public IScopeUnitOfWork CreateUnitOfWork(IsolationLevel isolationLevel, bool readOnly) + { + return new ScopeUnitOfWork(ScopeProvider, _repositoryFactory, isolationLevel, readOnly: readOnly); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/UnitOfWorkBase.cs b/src/Umbraco.Core/Persistence/UnitOfWork/UnitOfWorkBase.cs index eec524344c..80a19e6e19 100644 --- a/src/Umbraco.Core/Persistence/UnitOfWork/UnitOfWorkBase.cs +++ b/src/Umbraco.Core/Persistence/UnitOfWork/UnitOfWorkBase.cs @@ -9,11 +9,19 @@ namespace Umbraco.Core.Persistence.UnitOfWork { private readonly Queue _operations = new Queue(); - protected UnitOfWorkBase(RepositoryFactory repositoryFactory) + // fixme - explain readonly + // it means that the unit of work *will* complete no matter what + // but if an exception is thrown from within the 'using' block + // the calling code still can deal with it and cancel everything + + protected UnitOfWorkBase(RepositoryFactory repositoryFactory, bool readOnly = false) { RepositoryFactory = repositoryFactory; + ReadOnly = readOnly; } + protected bool ReadOnly { get; } + protected RepositoryFactory RepositoryFactory { get; } public abstract TRepository CreateRepository(string name = null) @@ -26,7 +34,11 @@ namespace Umbraco.Core.Persistence.UnitOfWork /// The repository in charge of the entity. public void RegisterCreated(IEntity entity, IUnitOfWorkRepository repository) { + if (ReadOnly) + throw new NotSupportedException("This unit of work is read-only."); + Completed = false; + _operations.Enqueue(new Operation { Entity = entity, @@ -42,7 +54,11 @@ namespace Umbraco.Core.Persistence.UnitOfWork /// The repository in charge of the entity. public void RegisterUpdated(IEntity entity, IUnitOfWorkRepository repository) { + if (ReadOnly) + throw new NotSupportedException("This unit of work is read-only."); + Completed = false; + _operations.Enqueue(new Operation { Entity = entity, @@ -58,7 +74,11 @@ namespace Umbraco.Core.Persistence.UnitOfWork /// The repository in charge of the entity. public void RegisterDeleted(IEntity entity, IUnitOfWorkRepository repository) { + if (ReadOnly) + throw new NotSupportedException("This unit of work is read-only."); + Completed = false; + _operations.Enqueue(new Operation { Entity = entity, @@ -67,11 +87,15 @@ namespace Umbraco.Core.Persistence.UnitOfWork }); } + // fixme - we don't need Begin, really, or do we? public virtual void Begin() { } public virtual void Flush() { + if (ReadOnly) + throw new NotSupportedException("This unit of work is read-only."); + Begin(); while (_operations.Count > 0) @@ -90,10 +114,19 @@ namespace Umbraco.Core.Persistence.UnitOfWork break; } } + + // fixme - if any operation throws, the uow still contains the remaining ops + // should we clear the ops? } public virtual void Complete() { + if (ReadOnly) + throw new NotSupportedException("This unit of work is read-only."); + + if (Completed) + throw new InvalidOperationException("This unit of work has already been completed."); + Flush(); Completed = true; } diff --git a/src/Umbraco.Core/Plugins/PluginManager.cs b/src/Umbraco.Core/Plugins/PluginManager.cs index 7980b8ab00..dc9a45f47b 100644 --- a/src/Umbraco.Core/Plugins/PluginManager.cs +++ b/src/Umbraco.Core/Plugins/PluginManager.cs @@ -10,6 +10,7 @@ using Umbraco.Core.Cache; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Persistence.Mappers; +using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.PropertyEditors; using Umbraco.Core._Legacy.PackageActions; using File = System.IO.File; @@ -292,7 +293,7 @@ namespace Umbraco.Core.Plugins /// Fails if the cache is missing or corrupt in any way. internal Attempt> TryGetCached(Type baseType, Type attributeType) { - var cache = _runtimeCache.GetCacheItem, IEnumerable>>(CacheKey, ReadCache, TimeSpan.FromMinutes(4)); + var cache = _runtimeCache.GetCacheItem, IEnumerable>>(CacheKey, ReadCacheSafe, TimeSpan.FromMinutes(4)); IEnumerable types; cache.TryGetValue(Tuple.Create(baseType == null ? string.Empty : baseType.FullName, attributeType == null ? string.Empty : attributeType.FullName), out types); @@ -301,6 +302,28 @@ namespace Umbraco.Core.Plugins : Attempt.Succeed(types); } + internal Dictionary, IEnumerable> ReadCacheSafe() + { + try + { + return ReadCache(); + } + catch + { + try + { + var filePath = GetPluginListFilePath(); + File.Delete(filePath); + } + catch + { + // on-purpose, does not matter + } + } + + return new Dictionary, IEnumerable>(); + } + internal Dictionary, IEnumerable> ReadCache() { var cache = new Dictionary, IEnumerable>(); @@ -625,7 +648,9 @@ namespace Umbraco.Core.Plugins if (added) { _types[listKey] = typeList; - UpdateCache(); + //if we are scanning then update the cache file + if (scan) + UpdateCache(); } _logger.Logger.Debug("Resolved {0}, caching ({1}).", () => ResolvedName(baseType, attributeType), () => added.ToString().ToLowerInvariant()); @@ -720,6 +745,7 @@ namespace Umbraco.Core.Plugins #endregion } + // fixme - extract! internal static class PluginManagerExtensions { /// @@ -777,5 +803,13 @@ namespace Umbraco.Core.Plugins { return mgr.ResolveTypesWithAttribute(); } + + /// + /// Gets all classes implementing ISqlSyntaxProvider and marked with the SqlSyntaxProviderAttribute. + /// + public static IEnumerable ResolveSqlSyntaxProviders(this PluginManager mgr) + { + return mgr.ResolveTypesWithAttribute(); + } } } diff --git a/src/Umbraco.Core/Plugins/TypeHelper.cs b/src/Umbraco.Core/Plugins/TypeHelper.cs index 6456d90e2d..fa7e48109b 100644 --- a/src/Umbraco.Core/Plugins/TypeHelper.cs +++ b/src/Umbraco.Core/Plugins/TypeHelper.cs @@ -63,7 +63,7 @@ namespace Umbraco.Core.Plugins // ReSharper disable once LoopCanBeConvertedToQuery - no! foreach (var a in assembly.GetReferencedAssemblies()) { - if (string.Equals(a.Name, name, StringComparison.OrdinalIgnoreCase)) return true; + if (string.Equals(a.Name, name, StringComparison.Ordinal)) return true; } return false; } diff --git a/src/Umbraco.Core/Properties/AssemblyInfo.cs b/src/Umbraco.Core/Properties/AssemblyInfo.cs index ceb3bf18f9..b78788d8c9 100644 --- a/src/Umbraco.Core/Properties/AssemblyInfo.cs +++ b/src/Umbraco.Core/Properties/AssemblyInfo.cs @@ -1,10 +1,8 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Security; -using System.Security.Permissions; -// General Information about an assembly is controlled through the following +// General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("Umbraco.Core")] @@ -12,37 +10,37 @@ using System.Security.Permissions; [assembly: AssemblyConfiguration("")] [assembly: AssemblyProduct("Umbraco CMS")] -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("130a6b5c-50e7-4df3-a0dd-e9e7eb0b7c5c")] +// Umbraco Cms [assembly: InternalsVisibleTo("umbraco")] [assembly: InternalsVisibleTo("Umbraco.Tests")] -[assembly: InternalsVisibleTo("Umbraco.Extensions")] -[assembly: InternalsVisibleTo("businesslogic")] -[assembly: InternalsVisibleTo("cms")] -[assembly: InternalsVisibleTo("umbraco.editorControls")] -[assembly: InternalsVisibleTo("umbraco.webservices")] +[assembly: InternalsVisibleTo("Umbraco.Extensions")] // fixme ? +[assembly: InternalsVisibleTo("businesslogic")] // fixme ? +[assembly: InternalsVisibleTo("cms")] // fixme ? +[assembly: InternalsVisibleTo("umbraco.webservices")] // fixme ? [assembly: InternalsVisibleTo("umbraco.datalayer")] -[assembly: InternalsVisibleTo("umbraco.MacroEngines")] - -[assembly: InternalsVisibleTo("umbraco.editorControls")] [assembly: InternalsVisibleTo("Umbraco.Tests")] [assembly: InternalsVisibleTo("Umbraco.Tests.Benchmarks")] -[assembly: InternalsVisibleTo("Umbraco.Core")] [assembly: InternalsVisibleTo("Umbraco.Web")] [assembly: InternalsVisibleTo("Umbraco.Web.UI")] [assembly: InternalsVisibleTo("UmbracoExamine")] -[assembly: InternalsVisibleTo("Concorde.Sync")] -[assembly: InternalsVisibleTo("Umbraco.VisualStudio")] -[assembly: InternalsVisibleTo("Umbraco.Courier.Core")] -[assembly: InternalsVisibleTo("Umbraco.Courier.Persistence")] -[assembly: InternalsVisibleTo("umbraco.providers")] +// Umbraco Deploy +[assembly: InternalsVisibleTo("Umbraco.Deploy")] +[assembly: InternalsVisibleTo("Umbraco.Deploy.UI")] +[assembly: InternalsVisibleTo("Umbraco.Deploy.Cloud")] + +// Umbraco Forms +[assembly: InternalsVisibleTo("Umbraco.Forms.Core")] +[assembly: InternalsVisibleTo("Umbraco.Forms.Core.Providers")] +[assembly: InternalsVisibleTo("Umbraco.Forms.Web")] //allow this to be mocked in our unit tests [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyValidator.cs b/src/Umbraco.Core/PropertyEditors/IPropertyValidator.cs index 655ed81568..3aa5958135 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyValidator.cs @@ -4,8 +4,6 @@ using Umbraco.Core.Models; namespace Umbraco.Core.PropertyEditors { - - /// /// An interface defining a validator /// diff --git a/src/Umbraco.Core/PropertyEditors/PreValueField.cs b/src/Umbraco.Core/PropertyEditors/PreValueField.cs index 3cf4e960ea..2b66f7a6a8 100644 --- a/src/Umbraco.Core/PropertyEditors/PreValueField.cs +++ b/src/Umbraco.Core/PropertyEditors/PreValueField.cs @@ -15,6 +15,7 @@ namespace Umbraco.Core.PropertyEditors public PreValueField() { Validators = new List(); + Config = new Dictionary(); //check for an attribute and fill the values var att = GetType().GetCustomAttribute(false); @@ -79,5 +80,11 @@ namespace Umbraco.Core.PropertyEditors /// [JsonProperty("validation", ItemConverterType = typeof(ManifestValidatorConverter))] public List Validators { get; private set; } + + /// + /// This allows for custom configuration to be injected into the pre-value editor + /// + [JsonProperty("config")] + public IDictionary Config { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditor.cs index 9bca3d6009..3f16fcb82e 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditor.cs @@ -21,17 +21,16 @@ namespace Umbraco.Core.PropertyEditors /// /// Exposes a logger /// - protected ILogger Logger { get; private set; } + protected ILogger Logger { get; } private readonly PropertyEditorAttribute _attribute; /// /// The constructor will setup the property editor based on the attribute if one is found /// - public PropertyEditor(ILogger logger) + public PropertyEditor(ILogger logger) { - if (logger == null) throw new ArgumentNullException("logger"); - Logger = logger; + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); //defaults Icon = Constants.Icons.PropertyEditor; Group = "common"; @@ -46,6 +45,7 @@ namespace Umbraco.Core.PropertyEditors IsParameterEditor = _attribute.IsParameterEditor; Icon = _attribute.Icon; Group = _attribute.Group; + IsDeprecated = _attribute.IsDeprecated; } } @@ -92,32 +92,23 @@ namespace Umbraco.Core.PropertyEditors public string Group { get; internal set; } - [JsonProperty("editor", Required = Required.Always)] - public PropertyValueEditor ValueEditor - { - get { return CreateValueEditor(); } - } + [JsonProperty("editor", Required = Required.Always)] + public PropertyValueEditor ValueEditor => CreateValueEditor(); [JsonIgnore] - IValueEditor IParameterEditor.ValueEditor - { - get { return ValueEditor; } - } + public bool IsDeprecated { get; internal set; } // fixme kill it all in v8 + + [JsonIgnore] + IValueEditor IParameterEditor.ValueEditor => ValueEditor; [JsonProperty("prevalues")] - public PreValueEditor PreValueEditor - { - get { return CreatePreValueEditor(); } - } + public PreValueEditor PreValueEditor => CreatePreValueEditor(); [JsonProperty("defaultConfig")] public virtual IDictionary DefaultPreValues { get; set; } [JsonIgnore] - IDictionary IParameterEditor.Configuration - { - get { return DefaultPreValues; } - } + IDictionary IParameterEditor.Configuration => DefaultPreValues; /// /// Creates a value editor instance @@ -155,7 +146,7 @@ namespace Umbraco.Core.PropertyEditors /// /// protected virtual PreValueEditor CreatePreValueEditor() - { + { //This will not be null if it is a manifest defined editor if (ManifestDefinedPreValueEditor != null) { @@ -165,7 +156,7 @@ namespace Umbraco.Core.PropertyEditors if (f.View.StartsWith("~/")) { f.View = IOHelper.ResolveUrl(f.View); - } + } } return ManifestDefinedPreValueEditor; } @@ -183,7 +174,7 @@ namespace Umbraco.Core.PropertyEditors { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; + if (obj.GetType() != GetType()) return false; return Equals((PropertyEditor) obj); } @@ -197,7 +188,7 @@ namespace Umbraco.Core.PropertyEditors /// protected virtual string DebuggerDisplay() { - return string.Format("Name: {0}, Alias: {1}, IsParameterEditor: {2}", Name, Alias, IsParameterEditor); + return $"Name: {Name}, Alias: {Alias}, IsParameterEditor: {IsParameterEditor}"; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorAttribute.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorAttribute.cs index d120753185..5777eb1dc0 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorAttribute.cs @@ -54,12 +54,18 @@ namespace Umbraco.Core.PropertyEditors Group = "common"; } - public string Alias { get; private set; } - public string Name { get; private set; } - public string EditorView { get; private set; } + public string Alias { get; } + public string Name { get; } + public string EditorView { get; } public string ValueType { get; set; } public bool IsParameterEditor { get; set; } + /// + /// If set to true, this property editor will not show up in the DataType's drop down list + /// if there is not already one of them chosen for a DataType + /// + public bool IsDeprecated { get; set; } // fixme should just kill in v8 + /// /// If this is is true than the editor will be displayed full width without a label /// diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs index 1062c9044b..add020228c 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs @@ -13,5 +13,11 @@ namespace Umbraco.Core.PropertyEditors // note: virtual so it can be mocked public virtual PropertyEditor this[string alias] => this.SingleOrDefault(x => x.Alias == alias); + + public virtual bool TryGet(string alias, out PropertyEditor editor) + { + editor = this.FirstOrDefault(x => x.Alias == alias); + return editor != null; + } } } diff --git a/src/Umbraco.Core/PropertyEditors/TagValueType.cs b/src/Umbraco.Core/PropertyEditors/TagValueType.cs index e5f65933ab..e837d1e8bf 100644 --- a/src/Umbraco.Core/PropertyEditors/TagValueType.cs +++ b/src/Umbraco.Core/PropertyEditors/TagValueType.cs @@ -14,7 +14,8 @@ /// The list of tags will be supplied by the property editor's ConvertEditorToDb method result which will need to return an IEnumerable{string} value /// /// - /// if the ConvertEditorToDb doesn't return an IEnumerable{string} then an exception will be thrown. + /// if the ConvertEditorToDb doesn't return an IEnumerable{string} then it will automatically try to be detected as either CSV or JSON and if neither of those match + /// an exception will be thrown. /// CustomTagList } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/CheckboxListValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/CheckboxListValueConverter.cs new file mode 100644 index 0000000000..3f7e1c377b --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/CheckboxListValueConverter.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + [DefaultPropertyValueConverter] + public class CheckboxListValueConverter : PropertyValueConverterBase + { + public override bool IsConverter(PublishedPropertyType propertyType) + { + return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.CheckBoxListAlias); + } + + public override Type GetPropertyValueType(PublishedPropertyType propertyType) + { + return typeof(IEnumerable); + } + + public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + { + return PropertyCacheLevel.Content; + } + + public override object ConvertInterToObject(PublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) + { + var sourceString = (source ?? string.Empty).ToString(); + + if (string.IsNullOrEmpty(sourceString)) + return Enumerable.Empty(); + + var values = + sourceString.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(v => v.Trim()); + + return values; + } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ColorPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ColorPickerValueConverter.cs index 66693503d0..d293215349 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/ColorPickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ColorPickerValueConverter.cs @@ -1,12 +1,24 @@ -using Umbraco.Core.Models.PublishedContent; +using System; +using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.PropertyEditors.ValueConverters { + [DefaultPropertyValueConverter] public class ColorPickerValueConverter : PropertyValueConverterBase { public override bool IsConverter(PublishedPropertyType propertyType) { - return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.ColorPickerAlias); + return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.ColorPickerAlias); + } + + public override Type GetPropertyValueType(PublishedPropertyType propertyType) + { + return typeof(string); + } + + public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + { + return PropertyCacheLevel.Content; } public override object ConvertSourceToInter(PublishedPropertyType propertyType, object source, bool preview) diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs index 1153000fee..40b9d73516 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs @@ -5,6 +5,7 @@ using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.PropertyEditors.ValueConverters { + [DefaultPropertyValueConverter] public class DatePickerValueConverter : PropertyValueConverterBase { private static readonly string[] PropertyEditorAliases = diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs index fc68202a01..75ded06120 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs @@ -4,6 +4,7 @@ using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.PropertyEditors.ValueConverters { + [DefaultPropertyValueConverter] public class DecimalValueConverter : PropertyValueConverterBase { public override bool IsConverter(PublishedPropertyType propertyType) diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListMultipleValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListMultipleValueConverter.cs new file mode 100644 index 0000000000..d44c234c50 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListMultipleValueConverter.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + [DefaultPropertyValueConverter] + public class DropdownListMultipleValueConverter : PropertyValueConverterBase + { + public override bool IsConverter(PublishedPropertyType propertyType) + { + return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.DropDownListMultipleAlias); + } + + public override Type GetPropertyValueType(PublishedPropertyType propertyType) + { + return typeof(IEnumerable); + } + + public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + { + return PropertyCacheLevel.Content; + } + + public override object ConvertInterToObject(PublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) + { + var sourceString = (source ?? "").ToString(); + + if (string.IsNullOrEmpty(sourceString)) + return Enumerable.Empty(); + + var values = + sourceString.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(v => v.Trim()); + + return values; + } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListMultipleWithKeysValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListMultipleWithKeysValueConverter.cs new file mode 100644 index 0000000000..efcf517cff --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListMultipleWithKeysValueConverter.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + [DefaultPropertyValueConverter] + public class DropdownListMultipleWithKeysValueConverter : PropertyValueConverterBase + { + public override bool IsConverter(PublishedPropertyType propertyType) + { + return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.DropdownlistMultiplePublishKeysAlias); + } + + public override Type GetPropertyValueType(PublishedPropertyType propertyType) + { + return typeof(IEnumerable); + } + + public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + { + return PropertyCacheLevel.Content; + } + + public override object ConvertSourceToInter(PublishedPropertyType propertyType, object source, bool preview) + { + if (source == null) + return new int[] { }; + + var prevalueIds = source.ToString() + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(p => p.Trim()) + .Select(int.Parse) + .ToArray(); + + return prevalueIds; + } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListValueConverter.cs new file mode 100644 index 0000000000..33a11d2b2a --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListValueConverter.cs @@ -0,0 +1,30 @@ +using System; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + [DefaultPropertyValueConverter] + public class DropdownListValueConverter : PropertyValueConverterBase + { + public override bool IsConverter(PublishedPropertyType propertyType) + { + return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.DropDownListAlias); + } + + public override Type GetPropertyValueType(PublishedPropertyType propertyType) + { + return typeof(string); + } + + public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + { + return PropertyCacheLevel.Content; + } + + public override object ConvertSourceToInter(PublishedPropertyType propertyType, object source, bool preview) + { + return source?.ToString() ?? string.Empty; + } + + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListWithKeysValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListWithKeysValueConverter.cs new file mode 100644 index 0000000000..67ca48375c --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListWithKeysValueConverter.cs @@ -0,0 +1,33 @@ +using System; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + [DefaultPropertyValueConverter] + public class DropdownListWithKeysValueConverter : PropertyValueConverterBase + { + public override bool IsConverter(PublishedPropertyType propertyType) + { + return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.DropdownlistPublishingKeysAlias); + } + + public override Type GetPropertyValueType(PublishedPropertyType propertyType) + { + return typeof(int); + } + + public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + { + return PropertyCacheLevel.Content; + } + + public override object ConvertSourceToInter(PublishedPropertyType propertyType, object source, bool preview) + { + var intAttempt = source.TryConvertTo(); + if (intAttempt.Success) + return intAttempt.Result; + + return null; + } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/EmailAddressValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/EmailAddressValueConverter.cs new file mode 100644 index 0000000000..47689b266a --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/EmailAddressValueConverter.cs @@ -0,0 +1,29 @@ +using System; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + [DefaultPropertyValueConverter] + public class EmailAddressValueConverter : PropertyValueConverterBase + { + public override bool IsConverter(PublishedPropertyType propertyType) + { + return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.EmailAddressAlias); + } + + public override Type GetPropertyValueType(PublishedPropertyType propertyType) + { + return typeof(string); + } + + public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + { + return PropertyCacheLevel.Content; + } + + public override object ConvertInterToObject(PublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) + { + return source.ToString(); + } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs index 42685359e6..c0e950a8e0 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs @@ -5,7 +5,6 @@ using System.Web; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.Grid; using Umbraco.Core.DI; using Umbraco.Core.IO; using Umbraco.Core.Logging; diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs index a0fb100d76..4327bb4017 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs @@ -3,6 +3,7 @@ using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.PropertyEditors.ValueConverters { + [DefaultPropertyValueConverter] public class IntegerValueConverter : PropertyValueConverterBase { public override bool IsConverter(PublishedPropertyType propertyType) diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs index f7a2a17571..543133546a 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs @@ -4,6 +4,7 @@ using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.PropertyEditors.ValueConverters { + [DefaultPropertyValueConverter] public class MarkdownEditorValueConverter : PropertyValueConverterBase { public override bool IsConverter(PublishedPropertyType propertyType) diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberGroupPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberGroupPickerValueConverter.cs new file mode 100644 index 0000000000..acf2b19d9e --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberGroupPickerValueConverter.cs @@ -0,0 +1,29 @@ +using System; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + [DefaultPropertyValueConverter] + public class MemberGroupPickerValueConverter : PropertyValueConverterBase + { + public override bool IsConverter(PublishedPropertyType propertyType) + { + return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.MemberGroupPickerAlias); + } + + public override Type GetPropertyValueType(PublishedPropertyType propertyType) + { + return typeof(string); + } + + public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + { + return PropertyCacheLevel.Content; + } + + public override object ConvertSourceToInter(PublishedPropertyType propertyType, object source, bool preview) + { + return source?.ToString() ?? string.Empty; + } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs index 5d1949f5a2..5940032b11 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs @@ -6,6 +6,7 @@ using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.PropertyEditors.ValueConverters { + [DefaultPropertyValueConverter] public class MultipleTextStringValueConverter : PropertyValueConverterBase { public override bool IsConverter(PublishedPropertyType propertyType) diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/RadioButtonListValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/RadioButtonListValueConverter.cs new file mode 100644 index 0000000000..7d52aab2c9 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/RadioButtonListValueConverter.cs @@ -0,0 +1,33 @@ +using System; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + [DefaultPropertyValueConverter] + public class RadioButtonListValueConverter : PropertyValueConverterBase + { + public override bool IsConverter(PublishedPropertyType propertyType) + { + return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.RadioButtonListAlias); + } + + public override Type GetPropertyValueType(PublishedPropertyType propertyType) + { + return typeof(int); + } + + public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + { + return PropertyCacheLevel.Content; + } + + public override object ConvertSourceToInter(PublishedPropertyType propertyType, object source, bool preview) + { + var intAttempt = source.TryConvertTo(); + if (intAttempt.Success) + return intAttempt.Result; + + return null; + } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs new file mode 100644 index 0000000000..3c76e3ce9a --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + [DefaultPropertyValueConverter] + public class SliderValueConverter : PropertyValueConverterBase + { + private readonly IDataTypeService _dataTypeService; + + public SliderValueConverter(IDataTypeService dataTypeService) + { + _dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); + } + + public override bool IsConverter(PublishedPropertyType propertyType) + { + return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.SliderAlias); + } + + public override Type GetPropertyValueType(PublishedPropertyType propertyType) + { + return IsRangeDataType(propertyType.DataTypeId) + ? typeof(Range) + : typeof(decimal); + } + + public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + { + return PropertyCacheLevel.Content; + } + + public override object ConvertInterToObject(PublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) + { + if (source == null) + { + return null; + } + + if (IsRangeDataType(propertyType.DataTypeId)) + { + var rangeRawValues = source.ToString().Split(','); + var minimumAttempt = rangeRawValues[0].TryConvertTo(); + var maximumAttempt = rangeRawValues[1].TryConvertTo(); + + if (minimumAttempt.Success && maximumAttempt.Success) + { + return new Range { Maximum = maximumAttempt.Result, Minimum = minimumAttempt.Result }; + } + } + + var valueAttempt = source.ToString().TryConvertTo(); + if (valueAttempt.Success) + { + return valueAttempt.Result; + } + + // Something failed in the conversion of the strings to decimals + return null; + + } + + /// + /// Discovers if the slider is set to range mode. + /// + /// + /// The data type id. + /// + /// + /// The . + /// + private bool IsRangeDataType(int dataTypeId) + { + // GetPreValuesCollectionByDataTypeId is cached at repository level; + // still, the collection is deep-cloned so this is kinda expensive, + // better to cache here + trigger refresh in DataTypeCacheRefresher + + return Storages.GetOrAdd(dataTypeId, id => + { + var preValue = _dataTypeService.GetPreValuesCollectionByDataTypeId(id) + .PreValuesAsDictionary + .FirstOrDefault(x => string.Equals(x.Key, "enableRange", StringComparison.InvariantCultureIgnoreCase)) + .Value; + + return preValue != null && preValue.Value.TryConvertTo().Result; + }); + } + + private static readonly ConcurrentDictionary Storages = new ConcurrentDictionary(); + + internal static void ClearCaches() + { + Storages.Clear(); + } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs new file mode 100644 index 0000000000..f846495c96 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + [DefaultPropertyValueConverter] + public class TagsValueConverter : PropertyValueConverterBase + { + private readonly IDataTypeService _dataTypeService; + + public TagsValueConverter(IDataTypeService dataTypeService) + { + _dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); + } + + public override bool IsConverter(PublishedPropertyType propertyType) + { + return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.TagsAlias); + } + + public override Type GetPropertyValueType(PublishedPropertyType propertyType) + { + return typeof(IEnumerable); + } + + public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + { + return PropertyCacheLevel.Content; + } + + public override object ConvertSourceToInter(PublishedPropertyType propertyType, object source, bool preview) + { + // if Json storage type deserialzie and return as string array + if (JsonStorageType(propertyType.DataTypeId)) + { + var jArray = JsonConvert.DeserializeObject(source.ToString()); + return jArray.ToObject(); + } + + // Otherwise assume CSV storage type and return as string array + var csvTags = + source.ToString() + .Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) + .ToArray(); + return csvTags; + } + + public override object ConvertInterToObject(PublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) + { + return (string[]) source; + } + + /// + /// Discovers if the tags data type is storing its data in a Json format + /// + /// + /// The data type id. + /// + /// + /// The . + /// + private bool JsonStorageType(int dataTypeId) + { + // GetPreValuesCollectionByDataTypeId is cached at repository level; + // still, the collection is deep-cloned so this is kinda expensive, + // better to cache here + trigger refresh in DataTypeCacheRefresher + + return Storages.GetOrAdd(dataTypeId, id => + { + var preValue = _dataTypeService.GetPreValuesCollectionByDataTypeId(id) + .PreValuesAsDictionary + .FirstOrDefault(x => string.Equals(x.Key, "storageType", StringComparison.InvariantCultureIgnoreCase)) + .Value; + + return preValue != null && preValue.Value.InvariantEquals("json"); + }); + } + + private static readonly ConcurrentDictionary Storages = new ConcurrentDictionary(); + + internal static void ClearCaches() + { + Storages.Clear(); + } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/TextStringValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/TextStringValueConverter.cs index 1b7f4407e0..d38a2b5b3c 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/TextStringValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/TextStringValueConverter.cs @@ -4,9 +4,10 @@ using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.PropertyEditors.ValueConverters { + [DefaultPropertyValueConverter] public class TextStringValueConverter : PropertyValueConverterBase { - private readonly static string[] PropertyTypeAliases = + private static readonly string[] PropertyTypeAliases = { Constants.PropertyEditors.TextboxAlias, Constants.PropertyEditors.TextboxMultipleAlias diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/TinyMceValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/TinyMceValueConverter.cs index cacd65de90..900d844957 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/TinyMceValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/TinyMceValueConverter.cs @@ -4,9 +4,10 @@ using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.PropertyEditors.ValueConverters { - /// - /// Value converter for the RTE so that it always returns IHtmlString so that Html.Raw doesn't have to be used. - /// + /// + /// Value converter for the RTE so that it always returns IHtmlString so that Html.Raw doesn't have to be used. + /// + [DefaultPropertyValueConverter] public class TinyMceValueConverter : PropertyValueConverterBase { public override bool IsConverter(PublishedPropertyType propertyType) diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/UploadPropertyConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/UploadPropertyConverter.cs new file mode 100644 index 0000000000..6b5634cbfe --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/UploadPropertyConverter.cs @@ -0,0 +1,56 @@ +using System; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + /// + /// The upload property value converter. + /// + [DefaultPropertyValueConverter] + public class UploadPropertyConverter : PropertyValueConverterBase + { + /// + /// Checks if this converter can convert the property editor and registers if it can. + /// + /// + /// The published property type. + /// + /// + /// The . + /// + public override bool IsConverter(PublishedPropertyType propertyType) + { + return propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.UploadFieldAlias); + } + + public override Type GetPropertyValueType(PublishedPropertyType propertyType) + { + return typeof(string); + } + + public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + { + return PropertyCacheLevel.Content; + } + + /// + /// Convert the source object to a string + /// + /// + /// The published property type. + /// + /// + /// The value of the property + /// + /// + /// The preview. + /// + /// + /// The . + /// + public override object ConvertInterToObject(PublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) + { + return source?.ToString() ?? ""; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs index 1fc69de502..ab9ec17195 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs @@ -3,6 +3,7 @@ using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.PropertyEditors.ValueConverters { + [DefaultPropertyValueConverter] public class YesNoValueConverter : PropertyValueConverterBase { public override bool IsConverter(PublishedPropertyType propertyType) diff --git a/src/Umbraco.Core/Scoping/IInstanceIdentifiable.cs b/src/Umbraco.Core/Scoping/IInstanceIdentifiable.cs new file mode 100644 index 0000000000..e855e68df2 --- /dev/null +++ b/src/Umbraco.Core/Scoping/IInstanceIdentifiable.cs @@ -0,0 +1,15 @@ +using System; + +namespace Umbraco.Core.Scoping +{ + /// + /// Exposes an instance unique identifier. + /// + public interface IInstanceIdentifiable + { + /// + /// Gets the instance unique identifier. + /// + Guid InstanceId { get; } + } +} diff --git a/src/Umbraco.Core/Scoping/IScope.cs b/src/Umbraco.Core/Scoping/IScope.cs new file mode 100644 index 0000000000..1c7d0dae43 --- /dev/null +++ b/src/Umbraco.Core/Scoping/IScope.cs @@ -0,0 +1,45 @@ +using System; +using Umbraco.Core.Cache; +using Umbraco.Core.Events; +using Umbraco.Core.Persistence; + +namespace Umbraco.Core.Scoping +{ + /// + /// Represents a scope. + /// + public interface IScope : IDisposable, IInstanceIdentifiable + { + /// + /// Gets the scope database. + /// + IUmbracoDatabase Database { get; } + + /// + /// Gets the scope event messages. + /// + EventMessages Messages { get; } + + /// + /// Gets the scope event dispatcher. + /// + IEventDispatcher Events { get; } + + /// + /// Gets the repositories cache mode. + /// + RepositoryCacheMode RepositoryCacheMode { get; } + + /// + /// Gets the scope isolated cache. + /// + IsolatedRuntimeCache IsolatedRuntimeCache { get; } + + /// + /// Completes the scope. + /// + /// A value indicating whether the scope has been successfully completed. + /// Can return false if any child scope has not completed. + bool Complete(); + } +} diff --git a/src/Umbraco.Core/Scoping/IScopeInternal.cs b/src/Umbraco.Core/Scoping/IScopeInternal.cs new file mode 100644 index 0000000000..220b6456a7 --- /dev/null +++ b/src/Umbraco.Core/Scoping/IScopeInternal.cs @@ -0,0 +1,56 @@ +using System.Data; +using Umbraco.Core.Events; +using Umbraco.Core.Persistence; + +namespace Umbraco.Core.Scoping +{ + /// + /// Provides additional, internal scope functionnalities. + /// + internal interface IScopeInternal : IScope // fixme - define what's internal and why + { + /// + /// Gets the parent scope, if any, or null. + /// + IScopeInternal ParentScope { get; } + + /// + /// Gets a value indicating whether this scope should be registered in + /// call context even when an http context is available. + /// + bool CallContext { get; } + + /// + /// Gets the scope transaction isolation level. + /// + IsolationLevel IsolationLevel { get; } + + /// + /// Gets the scope database, if any, else null. + /// + IUmbracoDatabase DatabaseOrNull { get; } + + /// + /// Get the scope event messages, if any, else null. + /// + EventMessages MessagesOrNull { get; } + + /// + /// Gets a value indicating whether filesystems are scoped. + /// + bool ScopedFileSystems { get; } + + /// + /// Registers that a child has completed. + /// + /// The child's completion status. + /// Completion status can be true (completed), false (could not complete), or null (not properly exited). + void ChildCompleted(bool? completed); + + /// + /// Resets the scope. + /// + /// Reset completion to "unspecified". + void Reset(); + } +} diff --git a/src/Umbraco.Core/Scoping/IScopeProvider.cs b/src/Umbraco.Core/Scoping/IScopeProvider.cs new file mode 100644 index 0000000000..6cdb7ec41a --- /dev/null +++ b/src/Umbraco.Core/Scoping/IScopeProvider.cs @@ -0,0 +1,82 @@ +using System; +using System.Data; +using Umbraco.Core.Events; +#if DEBUG_SCOPES +using System.Collections.Generic; +#endif + +namespace Umbraco.Core.Scoping +{ + /// + /// Provides scopes. + /// + public interface IScopeProvider + { + /// + /// Creates an ambient scope. + /// + /// The transaction isolation level. + /// The repositories cache mode. + /// An optional events dispatcher. + /// A value indicating whether to scope the filesystems. + /// fixme what is this + /// The created ambient scope. + /// + /// The created scope becomes the ambient scope. + /// If an ambient scope already exists, it becomes the parent of the created scope. + /// When the created scope is disposed, the parent scope becomes the ambient scope again. + /// Parameters must be specified on the outermost scope, or must be compatible with the parents. + /// + IScope CreateScope( + IsolationLevel isolationLevel = IsolationLevel.Unspecified, + RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, + IEventDispatcher eventDispatcher = null, + bool? scopeFileSystems = null, + bool callContext = false); + + /// + /// Creates a detached scope. + /// + /// A detached scope. + /// fixme - params! + /// + /// A detached scope is not ambient and has no parent. + /// It is meant to be attached by . + /// + IScope CreateDetachedScope( + IsolationLevel isolationLevel = IsolationLevel.Unspecified, + RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, + IEventDispatcher eventDispatcher = null, + bool? scopeFileSystems = null); + + /// + /// Attaches a scope. + /// + /// The scope to attach. + /// A value indicating whether to force usage of call context. + /// + /// Only a scope created by can be attached. + /// + void AttachScope(IScope scope, bool callContext = false); + + /// + /// Detaches a scope. + /// + /// The detached scope. + /// + /// Only a scope previously attached by can be detached. + /// + IScope DetachScope(); + + /// + /// Gets the scope context. + /// + ScopeContext Context { get; } + +#if DEBUG_SCOPES + Dictionary CallContextObjects { get; } + IEnumerable ScopeInfos { get; } + ScopeInfo GetScopeInfo(IScope scope); +#endif + } +} diff --git a/src/Umbraco.Core/Scoping/IScopeProviderInternal.cs b/src/Umbraco.Core/Scoping/IScopeProviderInternal.cs new file mode 100644 index 0000000000..6fc6776838 --- /dev/null +++ b/src/Umbraco.Core/Scoping/IScopeProviderInternal.cs @@ -0,0 +1,28 @@ +namespace Umbraco.Core.Scoping +{ + /// + /// Provides additional, internal scope provider functionnalities. + /// + /// Extends with internal features. + internal interface IScopeProviderInternal : IScopeProvider // fixme - define what's internal and why + { + /// + /// Gets the ambient context. + /// + /// May be null. + ScopeContext AmbientContext { get; } + + /// + /// Gets the ambient scope. + /// + /// May be null. + IScopeInternal AmbientScope { get; } + + /// + /// Resets the ambient scope. + /// + /// Resets the ambient scope (not completed anymore) and disposes the + /// entire scopes chain until there is no more scopes. + void Reset(); + } +} diff --git a/src/Umbraco.Core/Scoping/RepositoryCacheMode.cs b/src/Umbraco.Core/Scoping/RepositoryCacheMode.cs new file mode 100644 index 0000000000..23cf94d327 --- /dev/null +++ b/src/Umbraco.Core/Scoping/RepositoryCacheMode.cs @@ -0,0 +1,27 @@ +namespace Umbraco.Core.Scoping +{ + /// + /// Specifies the cache mode of repositories. + /// + public enum RepositoryCacheMode + { + /// + /// Unspecified. + /// + Unspecified = 0, + + /// + /// Default, full L2 cache. + /// + Default = 1, + + /// + /// Scoped cache. + /// + /// + /// Reads from, and writes to, a scope-local cache. + /// Upon scope completion, clears the global L2 cache. + /// + Scoped = 2 + } +} diff --git a/src/Umbraco.Core/Scoping/Scope.cs b/src/Umbraco.Core/Scoping/Scope.cs new file mode 100644 index 0000000000..321cf6d297 --- /dev/null +++ b/src/Umbraco.Core/Scoping/Scope.cs @@ -0,0 +1,478 @@ +using System; +using System.Data; +using System.Threading; +using Umbraco.Core.Cache; +using Umbraco.Core.Configuration; +using Umbraco.Core.Events; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; + +namespace Umbraco.Core.Scoping +{ + /// + /// Implements . + /// + /// Not thread-safe obviously. + internal class Scope : IScopeInternal + { + // fixme + // considering that a great amount of things here are only useful for the top-level + // scope would it make sense to have a ChildScope class that would have a smaller + // memory footprint? + + private readonly ScopeProvider _scopeProvider; + private readonly ILogger _logger; + + private readonly IsolationLevel _isolationLevel; + private readonly RepositoryCacheMode _repositoryCacheMode; + private readonly bool? _scopeFileSystem; + private readonly ScopeContext _scopeContext; + private bool _callContext; + + private bool _disposed; + private bool? _completed; + + private IsolatedRuntimeCache _isolatedRuntimeCache; + private IUmbracoDatabase _database; + private EventMessages _messages; + private ICompletable _fscope; + private IEventDispatcher _eventDispatcher; + + private const IsolationLevel DefaultIsolationLevel = IsolationLevel.RepeatableRead; + + // initializes a new scope + private Scope(ScopeProvider scopeProvider, + ILogger logger, FileSystems fileSystems, Scope parent, ScopeContext scopeContext, bool detachable, + IsolationLevel isolationLevel = IsolationLevel.Unspecified, + RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, + IEventDispatcher eventDispatcher = null, + bool? scopeFileSystems = null, + bool callContext = false) + { + _scopeProvider = scopeProvider; + _logger = logger; + + _scopeContext = scopeContext; + + _isolationLevel = isolationLevel; + _repositoryCacheMode = repositoryCacheMode; + _eventDispatcher = eventDispatcher; + _scopeFileSystem = scopeFileSystems; + _callContext = callContext; + + Detachable = detachable; + +#if DEBUG_SCOPES + _scopeProvider.RegisterScope(this); + Console.WriteLine("create " + _instanceId.ToString("N").Substring(0, 8)); +#endif + + if (detachable) + { + if (parent != null) throw new ArgumentException("Cannot set parent on detachable scope.", nameof(parent)); + if (scopeContext != null) throw new ArgumentException("Cannot set context on detachable scope.", nameof(scopeContext)); + + // detachable creates its own scope context + _scopeContext = new ScopeContext(); + + // see note below + if (scopeFileSystems == true) + _fscope = fileSystems.Shadow(Guid.NewGuid()); + + return; + } + + if (parent != null) + { + ParentScope = parent; + + // cannot specify a different mode! + if (repositoryCacheMode != RepositoryCacheMode.Unspecified && parent.RepositoryCacheMode != repositoryCacheMode) + throw new ArgumentException("Cannot be different from parent.", nameof(repositoryCacheMode)); + + // cannot specify a dispatcher! + if (_eventDispatcher != null) + throw new ArgumentException("Cannot be specified on nested scope.", nameof(eventDispatcher)); + + // cannot specify a different fs scope! + if (scopeFileSystems != null && parent._scopeFileSystem != scopeFileSystems) + throw new ArgumentException("Cannot be different from parent.", nameof(scopeFileSystems)); + } + else + { + // the FS scope cannot be "on demand" like the rest, because we would need to hook into + // every scoped FS to trigger the creation of shadow FS "on demand", and that would be + // pretty pointless since if scopeFileSystems is true, we *know* we want to shadow + if (scopeFileSystems == true) + _fscope = fileSystems.Shadow(Guid.NewGuid()); + } + } + + // initializes a new scope + public Scope(ScopeProvider scopeProvider, + ILogger logger, FileSystems fileSystems, bool detachable, ScopeContext scopeContext, + IsolationLevel isolationLevel = IsolationLevel.Unspecified, + RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, + IEventDispatcher eventDispatcher = null, + bool? scopeFileSystems = null, + bool callContext = false) + : this(scopeProvider, logger, fileSystems, null, scopeContext, detachable, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext) + { } + + // initializes a new scope in a nested scopes chain, with its parent + public Scope(ScopeProvider scopeProvider, + ILogger logger, FileSystems fileSystems, Scope parent, + IsolationLevel isolationLevel = IsolationLevel.Unspecified, + RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, + IEventDispatcher eventDispatcher = null, + bool? scopeFileSystems = null, + bool callContext = false) + : this(scopeProvider, logger, fileSystems, parent, null, false, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext) + { } + + public Guid InstanceId { get; } = Guid.NewGuid(); + + // a value indicating whether to force call-context + public bool CallContext + { + get + { + if (_callContext) return true; + if (ParentScope != null) return ParentScope.CallContext; + return false; + } + set => _callContext = value; + } + + public bool ScopedFileSystems + { + get + { + if (ParentScope != null) return ParentScope.ScopedFileSystems; + return _fscope != null; + } + } + + /// + public RepositoryCacheMode RepositoryCacheMode + { + get + { + if (_repositoryCacheMode != RepositoryCacheMode.Unspecified) return _repositoryCacheMode; + if (ParentScope != null) return ParentScope.RepositoryCacheMode; + return RepositoryCacheMode.Default; + } + } + + /// + public IsolatedRuntimeCache IsolatedRuntimeCache + { + get + { + if (ParentScope != null) return ParentScope.IsolatedRuntimeCache; + + return _isolatedRuntimeCache ?? (_isolatedRuntimeCache + = new IsolatedRuntimeCache(type => new DeepCloneRuntimeCacheProvider(new ObjectCacheRuntimeCacheProvider()))); + } + } + + // a value indicating whether the scope is detachable + // ie whether it was created by CreateDetachedScope + public bool Detachable { get; } + + // the parent scope (in a nested scopes chain) + public IScopeInternal ParentScope { get; set; } + + public bool Attached { get; set; } + + // the original scope (when attaching a detachable scope) + public IScopeInternal OrigScope { get; set; } + + // the original context (when attaching a detachable scope) + public ScopeContext OrigContext { get; set; } + + // the context (for attaching & detaching only) + public ScopeContext Context => _scopeContext; + + public IsolationLevel IsolationLevel + { + get + { + if (_isolationLevel != IsolationLevel.Unspecified) return _isolationLevel; + if (ParentScope != null) return ParentScope.IsolationLevel; + return DefaultIsolationLevel; + } + } + + /// + public IUmbracoDatabase Database + { + get + { + EnsureNotDisposed(); + + if (_database != null) + return _database; + + if (ParentScope != null) + { + var database = ParentScope.Database; + var currentLevel = database.GetCurrentTransactionIsolationLevel(); + if (_isolationLevel > IsolationLevel.Unspecified && currentLevel < _isolationLevel) + throw new Exception("Scope requires isolation level " + _isolationLevel + ", but got " + currentLevel + " from parent."); + return _database = database; + } + + // create a new database + _database = _scopeProvider.DatabaseFactory.CreateDatabase(); + + // enter a transaction, as a scope implies a transaction, always + try + { + _database.BeginTransaction(IsolationLevel); + return _database; + } + catch + { + _database.Dispose(); + _database = null; + throw; + } + } + } + + public IUmbracoDatabase DatabaseOrNull + { + get + { + EnsureNotDisposed(); + return ParentScope == null ? _database : ParentScope.DatabaseOrNull; + } + } + + /// + public EventMessages Messages + { + get + { + EnsureNotDisposed(); + if (ParentScope != null) return ParentScope.Messages; + return _messages ?? (_messages = new EventMessages()); + + // fixme - event messages? + // this may be a problem: the messages collection will be cleared at the end of the scope + // how shall we process it in controllers etc? if we don't want the global factory from v7? + // it'd need to be captured by the controller + // + // + rename // EventMessages = ServiceMessages or something + } + } + + public EventMessages MessagesOrNull + { + get + { + EnsureNotDisposed(); + return ParentScope == null ? _messages : ParentScope.MessagesOrNull; + } + } + + /// + public IEventDispatcher Events + { + get + { + EnsureNotDisposed(); + if (ParentScope != null) return ParentScope.Events; + return _eventDispatcher ?? (_eventDispatcher = new QueuingEventDispatcher()); + } + } + + /// + public bool Complete() + { + if (_completed.HasValue == false) + _completed = true; + return _completed.Value; + } + + public void Reset() + { + _completed = null; + } + + public void ChildCompleted(bool? completed) + { + // if child did not complete we cannot complete + if (completed.HasValue == false || completed.Value == false) + { + if (LogUncompletedScopes) + _logger.Debug("Uncompleted Child Scope at\r\n" + Environment.StackTrace); + _completed = false; + } + } + + private void EnsureNotDisposed() + { + if (_disposed) + throw new ObjectDisposedException(GetType().FullName); + + // fixme - safer? + //if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0) + // throw new ObjectDisposedException(GetType().FullName); + } + + public void Dispose() + { + EnsureNotDisposed(); + + if (this != _scopeProvider.AmbientScope) + { +#if DEBUG_SCOPES + var ambient = _scopeProvider.AmbientScope; + _logger.Debug("Dispose error (" + (ambient == null ? "no" : "other") + " ambient)"); + if (ambient == null) + throw new InvalidOperationException("Not the ambient scope (no ambient scope)."); + var infos = _scopeProvider.GetScopeInfo(ambient); + throw new InvalidOperationException("Not the ambient scope (see current ambient ctor stack trace).\r\n" + infos.CtorStack); +#else + throw new InvalidOperationException("Not the ambient scope."); +#endif + } + + var parent = ParentScope; + _scopeProvider.AmbientScope = parent; + +#if DEBUG_SCOPES + _scopeProvider.Disposed(this); +#endif + + if (parent != null) + parent.ChildCompleted(_completed); + else + DisposeLastScope(); + + _disposed = true; + GC.SuppressFinalize(this); + } + + private void DisposeLastScope() + { + // figure out completed + var completed = _completed.HasValue && _completed.Value; + + // deal with database + var databaseException = false; + if (_database != null) + { + try + { + if (completed) + _database.CompleteTransaction(); + else + _database.AbortTransaction(); + } + catch + { + databaseException = true; + throw; + } + finally + { + _database.Dispose(); + _database = null; + + if (databaseException) + RobustExit(false, true); + } + } + + RobustExit(completed, false); + } + + // this chains some try/finally blocks to + // - complete and dispose the scoped filesystems + // - deal with events if appropriate + // - remove the scope context if it belongs to this scope + // - deal with detachable scopes + // here, + // - completed indicates whether the scope has been completed + // can be true or false, but in both cases the scope is exiting + // in a normal way + // - onException indicates whether completing/aborting the database + // transaction threw an exception, in which case 'completed' has + // to be false + events don't trigger and we just to some cleanup + // to ensure we don't leave a scope around, etc + private void RobustExit(bool completed, bool onException) + { + if (onException) completed = false; + + TryFinally(() => + { + if (_scopeFileSystem == true) + { + if (completed) + _fscope.Complete(); + _fscope.Dispose(); + _fscope = null; + } + }, () => + { + // deal with events + if (onException == false) + _eventDispatcher?.ScopeExit(completed); + }, () => + { + // if *we* created it, then get rid of it + if (_scopeProvider.AmbientContext == _scopeContext) + { + try + { + _scopeProvider.AmbientContext.ScopeExit(completed); + } + finally + { + _scopeProvider.SetAmbient(null); + } + } + }, () => + { + if (Detachable) + { + // get out of the way, restore original + _scopeProvider.SetAmbient(OrigScope, OrigContext); + Attached = false; + OrigScope = null; + OrigContext = null; + } + }); + } + + private static void TryFinally(params Action[] actions) + { + TryFinally(0, actions); + } + + private static void TryFinally(int index, Action[] actions) + { + if (index == actions.Length) return; + try + { + actions[index](); + } + finally + { + TryFinally(index + 1, actions); + } + } + + // backing field for LogUncompletedScopes + private static bool? _logUncompletedScopes; + + // caching config + // true if Umbraco.CoreDebug.LogUncompletedScope appSetting is set to "true" + private static bool LogUncompletedScopes => (_logUncompletedScopes + ?? (_logUncompletedScopes = UmbracoConfig.For.CoreDebug().LogUncompletedScopes)).Value; + } +} diff --git a/src/Umbraco.Core/Scoping/ScopeContext.cs b/src/Umbraco.Core/Scoping/ScopeContext.cs new file mode 100644 index 0000000000..498285f8b0 --- /dev/null +++ b/src/Umbraco.Core/Scoping/ScopeContext.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Core.Scoping +{ + // fixme should we have an IScopeContext? + // fixme document all this properly! + + public class ScopeContext : IInstanceIdentifiable + { + private Dictionary _enlisted; + + public void ScopeExit(bool completed) + { + List exceptions = null; + foreach (var enlisted in Enlisted.Values) + { + try + { + enlisted.Execute(completed); + } + catch (Exception e) + { + if (exceptions == null) + exceptions = new List(); + exceptions.Add(e); + } + } + if (exceptions != null) + throw new AggregateException("Exceptions were thrown by listed actions.", exceptions); + } + + public Guid InstanceId { get; } = Guid.NewGuid(); + + private IDictionary Enlisted => _enlisted + ?? (_enlisted = new Dictionary()); + + private interface IEnlistedObject + { + void Execute(bool completed); + } + + private class EnlistedObject : IEnlistedObject + { + private readonly Action _action; + + public EnlistedObject(T item) + { + Item = item; + } + + public EnlistedObject(T item, Action action) + { + Item = item; + _action = action; + } + + public T Item { get; } + + public void Execute(bool completed) + { + _action(completed, Item); + } + } + + /// + public T Enlist(string key, Func creator) + { + IEnlistedObject enlisted; + if (Enlisted.TryGetValue(key, out enlisted)) + { + var enlistedAs = enlisted as EnlistedObject; + if (enlistedAs == null) throw new Exception("An item with a different type has already been enlisted with the same key."); + return enlistedAs.Item; + } + var enlistedOfT = new EnlistedObject(creator()); + Enlisted[key] = enlistedOfT; + return enlistedOfT.Item; + } + + /// + public T Enlist(string key, Func creator, Action action) + { + IEnlistedObject enlisted; + if (Enlisted.TryGetValue(key, out enlisted)) + { + var enlistedAs = enlisted as EnlistedObject; + if (enlistedAs == null) throw new Exception("An item with a different type has already been enlisted with the same key."); + return enlistedAs.Item; + } + var enlistedOfT = new EnlistedObject(creator(), action); + Enlisted[key] = enlistedOfT; + return enlistedOfT.Item; + } + + /// + public void Enlist(string key, Action action) + { + IEnlistedObject enlisted; + if (Enlisted.TryGetValue(key, out enlisted)) + { + var enlistedAs = enlisted as EnlistedObject; + if (enlistedAs == null) throw new Exception("An item with a different type has already been enlisted with the same key."); + return; + } + var enlistedOfT = new EnlistedObject(null, (completed, item) => action(completed)); + Enlisted[key] = enlistedOfT; + } + } +} diff --git a/src/Umbraco.Core/Scoping/ScopeProvider.cs b/src/Umbraco.Core/Scoping/ScopeProvider.cs new file mode 100644 index 0000000000..7333ef9af7 --- /dev/null +++ b/src/Umbraco.Core/Scoping/ScopeProvider.cs @@ -0,0 +1,570 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data; +using System.Runtime.Remoting.Messaging; +using System.Text; +using System.Web; +using Umbraco.Core.DI; +using Umbraco.Core.Events; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; +#if DEBUG_SCOPES +using System.Linq; +#endif + +namespace Umbraco.Core.Scoping +{ + /// + /// Implements . + /// + internal class ScopeProvider : IScopeProviderInternal + { + private readonly ILogger _logger; + private readonly Lazy _fileSystems; + + // fixme - circular dependency on file systems, refactor! + + public ScopeProvider(IUmbracoDatabaseFactory databaseFactory, Lazy fileSystems, ILogger logger) + { + DatabaseFactory = databaseFactory; + _fileSystems = fileSystems; + _logger = logger; + } + + static ScopeProvider() + { + SafeCallContext.Register( + () => + { + var scope = GetCallContextObject(ScopeItemKey); + var context = GetCallContextObject(ContextItemKey); + SetCallContextObject(ScopeItemKey, null); + SetCallContextObject(ContextItemKey, null); + return Tuple.Create(scope, context); + }, + o => + { + // cannot re-attached over leaked scope/context + if (GetCallContextObject(ScopeItemKey) != null) + throw new Exception("Found leaked scope when restoring call context."); + if (GetCallContextObject(ContextItemKey) != null) + throw new Exception("Found leaked context when restoring call context."); + + var t = (Tuple) o; + SetCallContextObject(ScopeItemKey, t.Item1); + SetCallContextObject(ContextItemKey, t.Item2); + }); + } + + public IUmbracoDatabaseFactory DatabaseFactory { get; } + + #region Context + + // objects that go into the logical call context better be serializable else they'll eventually + // cause issues whenever some cross-AppDomain code executes - could be due to ReSharper running + // tests, any other things (see https://msdn.microsoft.com/en-us/library/dn458353(v=vs.110).aspx), + // but we don't want to make all of our objects serializable since they are *not* meant to be + // used in cross-AppDomain scenario anyways. + // in addition, whatever goes into the logical call context is serialized back and forth any + // time cross-AppDomain code executes, so if we put an "object" there, we'll can *another* + // "object" instance - and so we cannot use a random object as a key. + // so what we do is: we register a guid in the call context, and we keep a table mapping those + // guids to the actual objects. the guid serializes back and forth without causing any issue, + // and we can retrieve the actual objects from the table. + // only issue: how are we supposed to clear the table? we can't, really. objects should take + // care of de-registering themselves from context. + // everything we use does, except the NoScope scope, which just stays there + // + // during tests, NoScope can to into call context... nothing much we can do about it + + private static readonly object StaticCallContextObjectsLock = new object(); + private static readonly Dictionary StaticCallContextObjects + = new Dictionary(); + +#if DEBUG_SCOPES + public Dictionary CallContextObjects + { + get + { + lock (StaticCallContextObjectsLock) + { + // capture in a dictionary + return StaticCallContextObjects.ToDictionary(x => x.Key, x => x.Value); + } + } + } +#endif + + private static T GetCallContextObject(string key) + where T : class + { + var objectKey = CallContext.LogicalGetData(key).AsGuid(); + if (objectKey == Guid.Empty) return null; + + lock (StaticCallContextObjectsLock) + { + if (StaticCallContextObjects.TryGetValue(objectKey, out object callContextObject)) + { +#if DEBUG_SCOPES + Logging.LogHelper.Debug("Got " + typeof(T).Name + " Object " + objectKey.ToString("N").Substring(0, 8)); + //Logging.LogHelper.Debug("At:\r\n" + Head(Environment.StackTrace, 24)); +#endif + return (T)callContextObject; + } + + // hard to inject into a static method :( + Current.Logger.Warn("Missed " + typeof(T).Name + " Object " + objectKey.ToString("N").Substring(0, 8)); +#if DEBUG_SCOPES + //Logging.LogHelper.Debug("At:\r\n" + Head(Environment.StackTrace, 24)); +#endif + return null; + } + } + + private static void SetCallContextObject(string key, IInstanceIdentifiable value) + { +#if DEBUG_SCOPES + // manage the 'context' that contains the scope (null, "http" or "call") + // only for scopes of course! + if (key == ScopeItemKey) + { + // first, null-register the existing value + var ambientKey = CallContext.LogicalGetData(ScopeItemKey).AsGuid(); + object o = null; + lock (StaticCallContextObjectsLock) + { + if (ambientKey != default(Guid)) + StaticCallContextObjects.TryGetValue(ambientKey, out o); + } + var ambientScope = o as IScope; + if (ambientScope != null) RegisterContext(ambientScope, null); + // then register the new value + var scope = value as IScope; + if (scope != null) RegisterContext(scope, "call"); + } +#endif + if (value == null) + { + var objectKey = CallContext.LogicalGetData(key).AsGuid(); + CallContext.FreeNamedDataSlot(key); + if (objectKey == default (Guid)) return; + lock (StaticCallContextObjectsLock) + { +#if DEBUG_SCOPES + Logging.LogHelper.Debug("Remove Object " + objectKey.ToString("N").Substring(0, 8)); + //Logging.LogHelper.Debug("At:\r\n" + Head(Environment.StackTrace, 24)); +#endif + StaticCallContextObjects.Remove(objectKey); + } + } + else + { + // note - we are *not* detecting an already-existing value + // because our code in this class *always* sets to null before + // setting to a real value + var objectKey = value.InstanceId; + lock (StaticCallContextObjectsLock) + { +#if DEBUG_SCOPES + Logging.LogHelper.Debug("AddObject " + objectKey.ToString("N").Substring(0, 8)); + //Logging.LogHelper.Debug("At:\r\n" + Head(Environment.StackTrace, 24)); +#endif + StaticCallContextObjects.Add(objectKey, value); + } + CallContext.LogicalSetData(key, objectKey); + } + } + + // this is for tests exclusively until we have a proper accessor in v8 + internal static Func HttpContextItemsGetter { get; set; } + + private static IDictionary HttpContextItems => HttpContextItemsGetter == null + ? HttpContext.Current?.Items + : HttpContextItemsGetter(); + + public static T GetHttpContextObject(string key, bool required = true) + where T : class + { + var httpContextItems = HttpContextItems; + if (httpContextItems != null) + return (T)httpContextItems[key]; + if (required) + throw new Exception("HttpContext.Current is null."); + return null; + } + + private static bool SetHttpContextObject(string key, object value, bool required = true) + { + var httpContextItems = HttpContextItems; + if (httpContextItems == null) + { + if (required) + throw new Exception("HttpContext.Current is null."); + return false; + } +#if DEBUG_SCOPES + // manage the 'context' that contains the scope (null, "http" or "call") + // only for scopes of course! + if (key == ScopeItemKey) + { + // first, null-register the existing value + var ambientScope = (IScope)httpContextItems[ScopeItemKey]; + if (ambientScope != null) RegisterContext(ambientScope, null); + // then register the new value + var scope = value as IScope; + if (scope != null) RegisterContext(scope, "http"); + } +#endif + if (value == null) + httpContextItems.Remove(key); + else + httpContextItems[key] = value; + return true; + } + +#endregion + + #region Ambient Context + + internal const string ContextItemKey = "Umbraco.Core.Scoping.ScopeContext"; + + internal static ScopeContext AmbientContextInternal + { + get + { + // try http context, fallback onto call context + var value = GetHttpContextObject(ContextItemKey, false); + return value ?? GetCallContextObject(ContextItemKey); + } + set + { + // clear both + SetHttpContextObject(ContextItemKey, null, false); + SetCallContextObject(ContextItemKey, null); + if (value == null) return; + + // set http/call context + if (SetHttpContextObject(ContextItemKey, value, false) == false) + SetCallContextObject(ContextItemKey, value); + } + } + + /// + public ScopeContext AmbientContext => AmbientContextInternal; + + #endregion + + #region Ambient Scope + + internal const string ScopeItemKey = "Umbraco.Core.Scoping.Scope"; + internal const string ScopeRefItemKey = "Umbraco.Core.Scoping.ScopeReference"; + + // only 1 instance which can be disposed and disposed again + // fixme - more weird static - we should try to get rid of all static & use an accessor + private static readonly ScopeReference StaticScopeReference = new ScopeReference(new ScopeProvider(null, null, null)); + + internal static IScopeInternal AmbientScopeInternal + { + get + { + // try http context, fallback onto call context + var value = GetHttpContextObject(ScopeItemKey, false); + return value ?? GetCallContextObject(ScopeItemKey); + } + set + { + // clear both + SetHttpContextObject(ScopeItemKey, null, false); + SetHttpContextObject(ScopeRefItemKey, null, false); + SetCallContextObject(ScopeItemKey, null); + if (value == null) return; + + // set http/call context + if (value.CallContext == false && SetHttpContextObject(ScopeItemKey, value, false)) + SetHttpContextObject(ScopeRefItemKey, StaticScopeReference); + else + SetCallContextObject(ScopeItemKey, value); + } + } + + /// + public IScopeInternal AmbientScope + { + get => AmbientScopeInternal; + internal set => AmbientScopeInternal = value; + } + + #endregion + + public void SetAmbient(IScopeInternal scope, ScopeContext context = null) + { + // clear all + SetHttpContextObject(ScopeItemKey, null, false); + SetHttpContextObject(ScopeRefItemKey, null, false); + SetCallContextObject(ScopeItemKey, null); + SetHttpContextObject(ContextItemKey, null, false); + SetCallContextObject(ContextItemKey, null); + if (scope == null) + { + if (context != null) + throw new ArgumentException("Must be null if scope is null.", nameof(context)); + return; + } + + if (scope.CallContext == false && SetHttpContextObject(ScopeItemKey, scope, false)) + { + SetHttpContextObject(ScopeRefItemKey, StaticScopeReference); + SetHttpContextObject(ContextItemKey, context); + } + else + { + SetCallContextObject(ScopeItemKey, scope); + SetCallContextObject(ContextItemKey, context); + } + } + + /// + public IScope CreateDetachedScope( + IsolationLevel isolationLevel = IsolationLevel.Unspecified, + RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, + IEventDispatcher eventDispatcher = null, + bool? scopeFileSystems = null) + { + return new Scope(this, _logger, _fileSystems.Value, true, null, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems); + } + + /// + public void AttachScope(IScope other, bool callContext = false) + { + var otherScope = other as Scope; + if (otherScope == null) + throw new ArgumentException("Not a Scope instance."); // fixme - why? how? + + if (otherScope.Detachable == false) + throw new ArgumentException("Not a detachable scope."); + + if (otherScope.Attached) + throw new InvalidOperationException("Already attached."); + + otherScope.Attached = true; + otherScope.OrigScope = AmbientScope; + otherScope.OrigContext = AmbientContext; + + otherScope.CallContext = callContext; + SetAmbient(otherScope, otherScope.Context); + } + + /// + public IScope DetachScope() + { + var ambient = AmbientScope; + if (ambient == null) + throw new InvalidOperationException("There is no ambient scope."); + + var scope = ambient as Scope; + if (scope == null) + throw new Exception("Ambient scope is not a Scope instance."); // fixme - why? how? + + if (scope.Detachable == false) + throw new InvalidOperationException("Ambient scope is not detachable."); + + SetAmbient(scope.OrigScope, scope.OrigContext); + scope.OrigScope = null; + scope.OrigContext = null; + scope.Attached = false; + return scope; + } + + /// + public IScope CreateScope( + IsolationLevel isolationLevel = IsolationLevel.Unspecified, + RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, + IEventDispatcher eventDispatcher = null, + bool? scopeFileSystems = null, + bool callContext = false) + { + var ambient = AmbientScope; + if (ambient == null) + { + var ambientContext = AmbientContext; + var newContext = ambientContext == null ? new ScopeContext() : null; + var scope = new Scope(this, _logger, _fileSystems.Value, false, newContext, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext); + // assign only if scope creation did not throw! + SetAmbient(scope, newContext ?? ambientContext); + return scope; + } + + var ambientScope = ambient as Scope; + if (ambientScope == null) throw new Exception("Ambient scope is not a Scope instance."); // fixme - why? how? + + var nested = new Scope(this, _logger, _fileSystems.Value, ambientScope, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext); + SetAmbient(nested, AmbientContext); + return nested; + } + + /// + public void Reset() + { + var scope = AmbientScope as Scope; + scope?.Reset(); + + StaticScopeReference.Dispose(); + } + + /// + public ScopeContext Context => AmbientContext; + +#if DEBUG_SCOPES + // this code needs TLC + // + // the idea here is to keep in a list all the scopes that have been created, and to remove them + // when they are disposed, so we can track leaks, ie scopes that would not be properly taken + // care of by our code + // + // note: the code could probably be optimized... but this is NOT supposed to go into any real + // live build, either production or debug - it's just a debugging tool for the time being + + // helps identifying when non-httpContext scopes are created by logging the stack trace + //private void LogCallContextStack() + //{ + // var trace = Environment.StackTrace; + // if (trace.IndexOf("ScheduledPublishing") > 0) + // LogHelper.Debug("CallContext: Scheduled Publishing"); + // else if (trace.IndexOf("TouchServerTask") > 0) + // LogHelper.Debug("CallContext: Server Registration"); + // else if (trace.IndexOf("LogScrubber") > 0) + // LogHelper.Debug("CallContext: Log Scrubber"); + // else + // LogHelper.Debug("CallContext: " + Environment.StackTrace); + //} + + // all scope instances that are currently beeing tracked + private static readonly object StaticScopeInfosLock = new object(); + private static readonly Dictionary StaticScopeInfos = new Dictionary(); + + public IEnumerable ScopeInfos + { + get + { + lock (StaticScopeInfosLock) + { + return StaticScopeInfos.Values.ToArray(); // capture in an array + } + } + } + + public ScopeInfo GetScopeInfo(IScope scope) + { + lock (StaticScopeInfosLock) + { + ScopeInfo scopeInfo; + return StaticScopeInfos.TryGetValue(scope, out scopeInfo) ? scopeInfo : null; + } + } + + //private static void Log(string message, UmbracoDatabase database) + //{ + // LogHelper.Debug(message + " (" + (database == null ? "" : database.InstanceSid) + ")."); + //} + + // register a scope and capture its ctor stacktrace + public void RegisterScope(IScope scope) + { + lock (StaticScopeInfosLock) + { + if (StaticScopeInfos.ContainsKey(scope)) throw new Exception("oops: already registered."); + Logging.LogHelper.Debug("Register " + scope.InstanceId.ToString("N").Substring(0, 8)); + StaticScopeInfos[scope] = new ScopeInfo(scope, Environment.StackTrace); + } + } + + // register that a scope is in a 'context' + // 'context' that contains the scope (null, "http" or "call") + public static void RegisterContext(IScope scope, string context) + { + lock (StaticScopeInfosLock) + { + ScopeInfo info; + if (StaticScopeInfos.TryGetValue(scope, out info) == false) info = null; + if (info == null) + { + if (context == null) return; + throw new Exception("oops: unregistered scope."); + } + var sb = new StringBuilder(); + var s = scope; + while (s != null) + { + if (sb.Length > 0) sb.Append(" < "); + sb.Append(s.InstanceId.ToString("N").Substring(0, 8)); + var ss = s as IScopeInternal; + s = ss == null ? null : ss.ParentScope; + } + Logging.LogHelper.Debug("Register " + (context ?? "null") + " context " + sb); + if (context == null) info.NullStack = Environment.StackTrace; + //Logging.LogHelper.Debug("At:\r\n" + Head(Environment.StackTrace, 16)); + info.Context = context; + } + } + + private static string Head(string s, int count) + { + var pos = 0; + var i = 0; + while (i < count && pos >= 0) + { + pos = s.IndexOf("\r\n", pos + 1, StringComparison.OrdinalIgnoreCase); + i++; + } + if (pos < 0) return s; + return s.Substring(0, pos); + } + + public void Disposed(IScope scope) + { + lock (StaticScopeInfosLock) + { + if (StaticScopeInfos.ContainsKey(scope)) + { + // enable this by default + //Console.WriteLine("unregister " + scope.InstanceId.ToString("N").Substring(0, 8)); + StaticScopeInfos.Remove(scope); + Logging.LogHelper.Debug("Remove " + scope.InstanceId.ToString("N").Substring(0, 8)); + + // instead, enable this to keep *all* scopes + // beware, there can be a lot of scopes! + //info.Disposed = true; + //info.DisposedStack = Environment.StackTrace; + } + } + } +#endif + } + +#if DEBUG_SCOPES + public class ScopeInfo + { + public ScopeInfo(IScope scope, string ctorStack) + { + Scope = scope; + Created = DateTime.Now; + CtorStack = ctorStack; + } + + public IScope Scope { get; private set; } // the scope itself + + // the scope's parent identifier + public Guid Parent { get { return (Scope is NoScope || ((Scope) Scope).ParentScope == null) ? Guid.Empty : ((Scope) Scope).ParentScope.InstanceId; } } + + public DateTime Created { get; private set; } // the date time the scope was created + public bool Disposed { get; set; } // whether the scope has been disposed already + public string Context { get; set; } // the current 'context' that contains the scope (null, "http" or "lcc") + + public string CtorStack { get; private set; } // the stacktrace of the scope ctor + public string DisposedStack { get; set; } // the stacktrace when disposed + public string NullStack { get; set; } // the stacktrace when the 'context' that contains the scope went null + } +#endif +} diff --git a/src/Umbraco.Core/Scoping/ScopeReference.cs b/src/Umbraco.Core/Scoping/ScopeReference.cs new file mode 100644 index 0000000000..998f21c587 --- /dev/null +++ b/src/Umbraco.Core/Scoping/ScopeReference.cs @@ -0,0 +1,30 @@ +namespace Umbraco.Core.Scoping +{ + /// + /// References a scope. + /// + /// Should go into HttpContext to indicate there is also an IScope in context + /// that needs to be disposed at the end of the request (the scope, and the entire scopes + /// chain). + internal class ScopeReference : IDisposeOnRequestEnd // implies IDisposable + { + private readonly IScopeProviderInternal _scopeProvider; + + public ScopeReference(IScopeProviderInternal scopeProvider) + { + _scopeProvider = scopeProvider; + } + + public void Dispose() + { + // dispose the entire chain (if any) + // reset (don't commit by default) + IScopeInternal scope; + while ((scope = _scopeProvider.AmbientScope) != null) + { + scope.Reset(); + scope.Dispose(); + } + } + } +} diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index 6475a6cf0c..7a5c13c2df 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -82,6 +82,40 @@ namespace Umbraco.Core.Security return false; } + /// + /// This will return the current back office identity if the IPrincipal is the correct type + /// + /// + /// + internal static UmbracoBackOfficeIdentity GetUmbracoIdentity(this IPrincipal user) + { + //If it's already a UmbracoBackOfficeIdentity + var backOfficeIdentity = user.Identity as UmbracoBackOfficeIdentity; + if (backOfficeIdentity != null) return backOfficeIdentity; + + //Check if there's more than one identity assigned and see if it's a UmbracoBackOfficeIdentity and use that + var claimsPrincipal = user as ClaimsPrincipal; + if (claimsPrincipal != null) + { + backOfficeIdentity = claimsPrincipal.Identities.OfType().FirstOrDefault(); + if (backOfficeIdentity != null) return backOfficeIdentity; + } + + //Otherwise convert to a UmbracoBackOfficeIdentity if it's auth'd and has the back office session + var claimsIdentity = user.Identity as ClaimsIdentity; + if (claimsIdentity != null && claimsIdentity.IsAuthenticated && claimsIdentity.HasClaim(x => x.Type == Constants.Security.SessionIdClaimType)) + { + try + { + return UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity); + } + catch (InvalidOperationException) + { + } + } + + return null; + } /// /// This will return the current back office identity. @@ -101,31 +135,8 @@ namespace Umbraco.Core.Security if (http.User == null) return null; //there's no user at all so no identity //If it's already a UmbracoBackOfficeIdentity - var backOfficeIdentity = http.User.Identity as UmbracoBackOfficeIdentity; - if (backOfficeIdentity != null) return backOfficeIdentity; - - //Check if there's more than one identity assigned and see if it's a UmbracoBackOfficeIdentity and use that - var claimsPrincipal = http.User as ClaimsPrincipal; - if (claimsPrincipal != null) - { - backOfficeIdentity = claimsPrincipal.Identities.OfType().FirstOrDefault(); - if (backOfficeIdentity != null) return backOfficeIdentity; - } - - //Otherwise convert to a UmbracoBackOfficeIdentity if it's auth'd and has the back office session - var claimsIdentity = http.User.Identity as ClaimsIdentity; - if (claimsIdentity != null && claimsIdentity.IsAuthenticated && claimsIdentity.HasClaim(x => x.Type == Constants.Security.SessionIdClaimType)) - { - try - { - return UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity); - } - catch (InvalidOperationException ex) - { - //This will occur if the required claim types are missing which would mean something strange is going on - Current.Logger.Error(typeof(AuthenticationExtensions), "The current identity cannot be converted to " + typeof(UmbracoBackOfficeIdentity), ex); - } - } + var backOfficeIdentity = GetUmbracoIdentity(http.User); + if (backOfficeIdentity != null) return backOfficeIdentity; if (authenticateRequestIfNotFound == false) return null; diff --git a/src/Umbraco.Core/Security/BackOfficeSignInManager.cs b/src/Umbraco.Core/Security/BackOfficeSignInManager.cs index 7c65b43291..71bd2474cd 100644 --- a/src/Umbraco.Core/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeSignInManager.cs @@ -35,7 +35,7 @@ namespace Umbraco.Core.Security public static BackOfficeSignInManager Create(IdentityFactoryOptions options, IOwinContext context, ILogger logger) { return new BackOfficeSignInManager( - context.GetBackOfficeUserManager(), + context.GetBackOfficeUserManager(), context.Authentication, logger, context.Request); @@ -48,8 +48,8 @@ namespace Umbraco.Core.Security /// public override async Task PasswordSignInAsync(string userName, string password, bool isPersistent, bool shouldLockout) { - var result = await base.PasswordSignInAsync(userName, password, isPersistent, shouldLockout); - + var result = await PasswordSignInAsyncImpl(userName, password, isPersistent, shouldLockout); + switch (result) { case SignInStatus.Success: @@ -69,7 +69,7 @@ namespace Umbraco.Core.Security case SignInStatus.RequiresVerification: _logger.WriteCore(TraceEventType.Information, 0, string.Format( - "Login attempt failed for username {0} from IP address {1}, the user requires verification", + "Login attempt requires verification for username {0} from IP address {1}", userName, _request.RemoteIpAddress), null, null); break; @@ -87,6 +87,68 @@ namespace Umbraco.Core.Security return result; } + /// + /// Borrowed from Micorosoft's underlying sign in manager which is not flexible enough to tell it to use a different cookie type + /// + /// + /// + /// + /// + /// + private async Task PasswordSignInAsyncImpl(string userName, string password, bool isPersistent, bool shouldLockout) + { + if (UserManager == null) + { + return SignInStatus.Failure; + } + var user = await UserManager.FindByNameAsync(userName); + if (user == null) + { + return SignInStatus.Failure; + } + if (await UserManager.IsLockedOutAsync(user.Id)) + { + return SignInStatus.LockedOut; + } + if (await UserManager.CheckPasswordAsync(user, password)) + { + await UserManager.ResetAccessFailedCountAsync(user.Id); + return await SignInOrTwoFactor(user, isPersistent); + } + if (shouldLockout) + { + // If lockout is requested, increment access failed count which might lock out the user + await UserManager.AccessFailedAsync(user.Id); + if (await UserManager.IsLockedOutAsync(user.Id)) + { + return SignInStatus.LockedOut; + } + } + return SignInStatus.Failure; + } + + /// + /// Borrowed from Micorosoft's underlying sign in manager which is not flexible enough to tell it to use a different cookie type + /// + /// + /// + /// + private async Task SignInOrTwoFactor(BackOfficeIdentityUser user, bool isPersistent) + { + var id = Convert.ToString(user.Id); + if (await UserManager.GetTwoFactorEnabledAsync(user.Id) + && (await UserManager.GetValidTwoFactorProvidersAsync(user.Id)).Count > 0) + { + var identity = new ClaimsIdentity(Constants.Security.BackOfficeTwoFactorAuthenticationType); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, id)); + identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, user.UserName)); + AuthenticationManager.SignIn(identity); + return SignInStatus.RequiresVerification; + } + await SignInAsync(user, isPersistent, false); + return SignInStatus.Success; + } + /// /// Creates a user identity and then signs the identity using the AuthenticationManager /// @@ -100,11 +162,11 @@ namespace Umbraco.Core.Security // Clear any partial cookies from external or two factor partial sign ins AuthenticationManager.SignOut( - Constants.Security.BackOfficeExternalAuthenticationType, + Constants.Security.BackOfficeExternalAuthenticationType, Constants.Security.BackOfficeTwoFactorAuthenticationType); var nowUtc = DateTime.Now.ToUniversalTime(); - + if (rememberBrowser) { var rememberBrowserIdentity = AuthenticationManager.CreateTwoFactorRememberBrowserIdentity(ConvertIdToString(user.Id)); @@ -114,7 +176,7 @@ namespace Umbraco.Core.Security AllowRefresh = true, IssuedUtc = nowUtc, ExpiresUtc = nowUtc.AddMinutes(GlobalSettings.TimeOutInMinutes) - }, userIdentity, rememberBrowserIdentity); + }, userIdentity, rememberBrowserIdentity); } else { @@ -127,11 +189,46 @@ namespace Umbraco.Core.Security }, userIdentity); } + //track the last login date + user.LastLoginDateUtc = DateTime.UtcNow; + await UserManager.UpdateAsync(user); + _logger.WriteCore(TraceEventType.Information, 0, string.Format( "Login attempt succeeded for username {0} from IP address {1}", user.UserName, _request.RemoteIpAddress), null, null); } + + /// + /// Get the user id that has been verified already or -1. + /// + /// + /// + /// Replaces the underlying call which is not flexible and doesn't support a custom cookie + /// + public new async Task GetVerifiedUserIdAsync() + { + var result = await AuthenticationManager.AuthenticateAsync(Constants.Security.BackOfficeTwoFactorAuthenticationType); + if (result != null && result.Identity != null && string.IsNullOrEmpty(result.Identity.GetUserId()) == false) + { + return ConvertIdFromString(result.Identity.GetUserId()); + } + return -1; + } + + /// + /// Get the username that has been verified already or null. + /// + /// + public async Task GetVerifiedUserNameAsync() + { + var result = await AuthenticationManager.AuthenticateAsync(Constants.Security.BackOfficeTwoFactorAuthenticationType); + if (result != null && result.Identity != null && string.IsNullOrEmpty(result.Identity.GetUserName()) == false) + { + return result.Identity.GetUserName(); + } + return null; + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index a2cf71c010..5a26ec1021 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -627,6 +627,12 @@ namespace Umbraco.Core.Security var anythingChanged = false; //don't assign anything if nothing has changed as this will trigger //the track changes of the model + if ((user.LastLoginDate != default(DateTime) && identityUser.LastLoginDateUtc.HasValue == false) + || identityUser.LastLoginDateUtc.HasValue && user.LastLoginDate.ToUniversalTime() != identityUser.LastLoginDateUtc.Value) + { + anythingChanged = true; + user.LastLoginDate = identityUser.LastLoginDateUtc.Value.ToLocalTime(); + } if (user.Name != identityUser.Name && identityUser.Name.IsNullOrWhiteSpace() == false) { anythingChanged = true; diff --git a/src/Umbraco.Core/Security/MembershipProviderBase.cs b/src/Umbraco.Core/Security/MembershipProviderBase.cs index edb4680ff8..d9cad61d1d 100644 --- a/src/Umbraco.Core/Security/MembershipProviderBase.cs +++ b/src/Umbraco.Core/Security/MembershipProviderBase.cs @@ -10,6 +10,7 @@ using System.Web.Hosting; using System.Web.Security; using Umbraco.Core.DI; using Umbraco.Core.Logging; +using Umbraco.Core.Models; namespace Umbraco.Core.Security { @@ -77,7 +78,8 @@ namespace Umbraco.Core.Security private bool _requiresQuestionAndAnswer; private bool _requiresUniqueEmail; private string _customHashAlgorithmType ; - internal bool UseLegacyEncoding; + + public bool UseLegacyEncoding { get; private set; } #region Properties @@ -324,6 +326,14 @@ namespace Umbraco.Core.Security throw new MembershipPasswordException("Change password canceled due to password validation failure."); } + //Special case to allow changing password without validating existing credentials + //This is used during installation only + var installing = Current.RuntimeState.Level == RuntimeLevel.Install; + if (AllowManuallyChangingPassword == false && installing && oldPassword == "default") + { + return PerformChangePassword(username, oldPassword, newPassword); + } + if (AllowManuallyChangingPassword == false) { if (ValidateUser(username, oldPassword) == false) return false; @@ -512,7 +522,11 @@ namespace Umbraco.Core.Security public override string ResetPassword(string username, string answer) { - if (EnablePasswordReset == false) + var userService = Current.Services.UserService; + + var canReset = this.CanResetPassword(userService); + + if (canReset == false) { throw new NotSupportedException("Password reset is not supported"); } diff --git a/src/Umbraco.Core/Security/MembershipProviderExtensions.cs b/src/Umbraco.Core/Security/MembershipProviderExtensions.cs index bdd3174960..645de22ab8 100644 --- a/src/Umbraco.Core/Security/MembershipProviderExtensions.cs +++ b/src/Umbraco.Core/Security/MembershipProviderExtensions.cs @@ -9,12 +9,45 @@ using System.Web; using System.Web.Hosting; using System.Web.Security; using Umbraco.Core.Configuration; +using Umbraco.Core.Models; using Umbraco.Core.Security; +using Umbraco.Core.Services; namespace Umbraco.Core.Security { public static class MembershipProviderExtensions { + /// + /// Extension method to check if a password can be reset based on a given provider and the current request (logged in user) + /// + /// + /// + /// + internal static bool CanResetPassword(this MembershipProvider provider, IUserService userService) + { + if (provider == null) throw new ArgumentNullException("provider"); + + var canReset = provider.EnablePasswordReset; + + if (userService == null) return canReset; + + //we need to check for the special case in which a user is an admin - in which acse they can reset the password even if EnablePasswordReset == false + if (provider.EnablePasswordReset == false) + { + var identity = Thread.CurrentPrincipal.GetUmbracoIdentity(); + if (identity != null) + { + var user = userService.GetByUsername(identity.Username); + var userIsAdmin = user.IsAdmin(); + if (userIsAdmin) + { + canReset = true; + } + } + } + return canReset; + } + internal static MembershipUserCollection FindUsersByName(this MembershipProvider provider, string usernameToMatch) { int totalRecords = 0; diff --git a/src/Umbraco.Core/Serialization/StreamResultExtensions.cs b/src/Umbraco.Core/Serialization/StreamResultExtensions.cs new file mode 100644 index 0000000000..96490a933c --- /dev/null +++ b/src/Umbraco.Core/Serialization/StreamResultExtensions.cs @@ -0,0 +1,22 @@ +using System.IO; +using System.Text; +using System.Xml.Linq; + +namespace Umbraco.Core.Serialization +{ + public static class StreamResultExtensions + { + public static string ToJsonString(this Stream stream) + { + byte[] bytes = new byte[stream.Length]; + stream.Position = 0; + stream.Read(bytes, 0, (int)stream.Length); + return Encoding.UTF8.GetString(bytes); + } + + public static XDocument ToXDoc(this Stream stream) + { + return XDocument.Load(stream); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Serialization/StreamedResult.cs b/src/Umbraco.Core/Serialization/StreamedResult.cs index 9b73fd06bf..0a50751229 100644 --- a/src/Umbraco.Core/Serialization/StreamedResult.cs +++ b/src/Umbraco.Core/Serialization/StreamedResult.cs @@ -1,6 +1,4 @@ using System.IO; -using System.Text; -using System.Xml.Linq; namespace Umbraco.Core.Serialization { @@ -20,20 +18,4 @@ namespace Umbraco.Core.Serialization #endregion } - - public static class StreamResultExtensions - { - public static string ToJsonString(this Stream stream) - { - byte[] bytes = new byte[stream.Length]; - stream.Position = 0; - stream.Read(bytes, 0, (int)stream.Length); - return Encoding.UTF8.GetString(bytes); - } - - public static XDocument ToXDoc(this Stream stream) - { - return XDocument.Load(stream); - } - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Serialization/UdiJsonConverter.cs b/src/Umbraco.Core/Serialization/UdiJsonConverter.cs new file mode 100644 index 0000000000..ff62535825 --- /dev/null +++ b/src/Umbraco.Core/Serialization/UdiJsonConverter.cs @@ -0,0 +1,27 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Umbraco.Core.Serialization +{ + + public class UdiJsonConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return typeof(Udi).IsAssignableFrom(objectType); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(value.ToString()); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var jo = JToken.ReadFrom(reader); + var val = jo.ToObject(); + return val == null ? null : Udi.Parse(val); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Serialization/UdiRangeJsonConverter.cs b/src/Umbraco.Core/Serialization/UdiRangeJsonConverter.cs new file mode 100644 index 0000000000..099c46f29d --- /dev/null +++ b/src/Umbraco.Core/Serialization/UdiRangeJsonConverter.cs @@ -0,0 +1,26 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Umbraco.Core.Serialization +{ + public class UdiRangeJsonConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return typeof(UdiRange).IsAssignableFrom(objectType); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(value.ToString()); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var jo = JToken.ReadFrom(reader); + var val = jo.ToObject(); + return val == null ? null : UdiRange.Parse(val); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/AuditService.cs b/src/Umbraco.Core/Services/AuditService.cs index 6cc858f6e8..081767680e 100644 --- a/src/Umbraco.Core/Services/AuditService.cs +++ b/src/Umbraco.Core/Services/AuditService.cs @@ -3,19 +3,16 @@ using System.Collections.Generic; using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Services { - public sealed class AuditService : RepositoryService, IAuditService + public sealed class AuditService : ScopeRepositoryService, IAuditService { - public AuditService(IDatabaseUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory) + public AuditService(IScopeUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory) : base(provider, logger, eventMessagesFactory) - { - } + { } public void Add(AuditType type, string comment, int userId, int objectId) { diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 78fccb0905..0240bad70a 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using Umbraco.Core.Events; +using Umbraco.Core.Exceptions; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; @@ -18,18 +19,14 @@ namespace Umbraco.Core.Services /// /// Represents the Content Service, which is an easy access to operations involving /// - public class ContentService : RepositoryService, IContentService, IContentServiceOperations + public class ContentService : ScopeRepositoryService, IContentService, IContentServiceOperations { private readonly MediaFileSystem _mediaFileSystem; private IQuery _queryNotTrashed; #region Constructors - public ContentService( - IDatabaseUnitOfWorkProvider provider, - ILogger logger, - IEventMessagesFactory eventMessagesFactory, - MediaFileSystem mediaFileSystem) + public ContentService(IScopeUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, MediaFileSystem mediaFileSystem) : base(provider, logger, eventMessagesFactory) { _mediaFileSystem = mediaFileSystem; @@ -41,7 +38,7 @@ namespace Umbraco.Core.Services // lazy-constructed because when the ctor runs, the query factory may not be ready - private IQuery QueryNotTrashed => _queryNotTrashed ?? (_queryNotTrashed = UowProvider.DatabaseFactory.Query().Where(x => x.Trashed == false)); + private IQuery QueryNotTrashed => _queryNotTrashed ?? (_queryNotTrashed = UowProvider.DatabaseContext.Query().Where(x => x.Trashed == false)); #endregion @@ -49,49 +46,41 @@ namespace Umbraco.Core.Services public int CountPublished(string contentTypeAlias = null) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repo = uow.CreateRepository(); - var count = repo.CountPublished(); - uow.Complete(); - return count; + return repo.CountPublished(); } } public int Count(string contentTypeAlias = null) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repo = uow.CreateRepository(); - var count = repo.Count(contentTypeAlias); - uow.Complete(); - return count; + return repo.Count(contentTypeAlias); } } public int CountChildren(int parentId, string contentTypeAlias = null) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repo = uow.CreateRepository(); - var count = repo.CountChildren(parentId, contentTypeAlias); - uow.Complete(); - return count; + return repo.CountChildren(parentId, contentTypeAlias); } } public int CountDescendants(int parentId, string contentTypeAlias = null) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repo = uow.CreateRepository(); - var count = repo.CountDescendants(parentId, contentTypeAlias); - uow.Complete(); - return count; + return repo.CountDescendants(parentId, contentTypeAlias); } } @@ -299,7 +288,7 @@ namespace Umbraco.Core.Services } } - private void CreateContent(IDatabaseUnitOfWork uow, Content content, IContent parent, int userId, bool withIdentity) + private void CreateContent(IScopeUnitOfWork uow, Content content, IContent parent, int userId, bool withIdentity) { // NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. @@ -307,7 +296,7 @@ namespace Umbraco.Core.Services ? new NewEventArgs(content, content.ContentType.Alias, parent) : new NewEventArgs(content, content.ContentType.Alias, -1); - if (Creating.IsRaisedEventCancelled(newArgs, this)) + if (uow.Events.DispatchCancelable(Creating, this, newArgs)) { content.WasCancelled = true; return; @@ -318,7 +307,7 @@ namespace Umbraco.Core.Services if (withIdentity) { - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) + if (uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(content))) { content.WasCancelled = true; return; @@ -329,16 +318,16 @@ namespace Umbraco.Core.Services uow.Flush(); // need everything so we can serialize - Saved.RaiseEvent(new SaveEventArgs(content, false), this); - TreeChanged.RaiseEvent(new TreeChange(content, TreeChangeTypes.RefreshNode).ToEventArgs(), this); + uow.Events.Dispatch(Saved, this, new SaveEventArgs(content, false)); + uow.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshNode).ToEventArgs()); } - Created.RaiseEvent(new NewEventArgs(content, false, content.ContentType.Alias, parent), this); + uow.Events.Dispatch(Created, this, new NewEventArgs(content, false, content.ContentType.Alias, parent)); var msg = withIdentity ? "Content '{0}' was created with Id {1}" : "Content '{0}' was created"; - Audit(AuditType.New, string.Format(msg, content.Name, content.Id), content.CreatorId, content.Id); + Audit(uow, AuditType.New, string.Format(msg, content.Name, content.Id), content.CreatorId, content.Id); } #endregion @@ -352,13 +341,11 @@ namespace Umbraco.Core.Services /// public IContent GetById(int id) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); - var content = repository.Get(id); - uow.Complete(); - return content; + return repository.Get(id); } } @@ -372,20 +359,15 @@ namespace Umbraco.Core.Services var idsA = ids.ToArray(); if (idsA.Length == 0) return Enumerable.Empty(); - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var items = repository.GetAll(idsA); - uow.Complete(); var index = items.ToDictionary(x => x.Id, x => x); - return idsA.Select(x => - { - IContent c; - return index.TryGetValue(x, out c) ? c : null; - }).WhereNotNull(); + return idsA.Select(x => index.TryGetValue(x, out IContent c) ? c : null).WhereNotNull(); } } @@ -396,14 +378,12 @@ namespace Umbraco.Core.Services /// public IContent GetById(Guid key) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.QueryT.Where(x => x.Key == key); - var content = repository.GetByQuery(query).SingleOrDefault(); - uow.Complete(); - return content; + return repository.GetByQuery(query).SingleOrDefault(); } } @@ -414,27 +394,23 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetContentOfContentType(int id) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.QueryT.Where(x => x.ContentTypeId == id); - var items = repository.GetByQuery(query); - uow.Complete(); - return items; + return repository.GetByQuery(query); } } internal IEnumerable GetPublishedContentOfContentType(int id) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.QueryT.Where(x => x.ContentTypeId == id); - var content = repository.GetByPublishedVersion(query); - uow.Complete(); - return content; + return repository.GetByPublishedVersion(query); } } @@ -446,14 +422,12 @@ namespace Umbraco.Core.Services /// Contrary to most methods, this method filters out trashed content items. public IEnumerable GetByLevel(int level) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.QueryT.Where(x => x.Level == level && x.Trashed == false); - var items = repository.GetByQuery(query); - uow.Complete(); - return items; + return repository.GetByQuery(query); } } @@ -464,13 +438,11 @@ namespace Umbraco.Core.Services /// An item public IContent GetByVersion(Guid versionId) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); - var content = repository.GetByVersion(versionId); - uow.Complete(); - return content; + return repository.GetByVersion(versionId); } } @@ -481,13 +453,11 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetVersions(int id) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); - var versions = repository.GetAllVersions(id); - uow.Complete(); - return versions; + return repository.GetAllVersions(id); } } @@ -499,12 +469,10 @@ namespace Umbraco.Core.Services /// public IEnumerable GetVersionIds(int id, int maxRows) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); - var versions = repository.GetVersionIds(id, maxRows); - uow.Complete(); - return versions; + return repository.GetVersionIds(id, maxRows); } } @@ -536,13 +504,11 @@ namespace Umbraco.Core.Services if (ids.Any() == false) return new List(); - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); - var ancestors = repository.GetAll(ids); - uow.Complete(); - return ancestors; + return repository.GetAll(ids); } } @@ -553,14 +519,12 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetChildren(int id) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.QueryT.Where(x => x.ParentId == id); - var children = repository.GetByQuery(query).OrderBy(x => x.SortOrder); - uow.Complete(); - return children; + return repository.GetByQuery(query).OrderBy(x => x.SortOrder); } } @@ -571,14 +535,12 @@ namespace Umbraco.Core.Services /// An Enumerable list of published objects public IEnumerable GetPublishedChildren(int id) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.QueryT.Where(x => x.ParentId == id && x.Published); - var children = repository.GetByQuery(query).OrderBy(x => x.SortOrder); - uow.Complete(); - return children; + return repository.GetByQuery(query).OrderBy(x => x.SortOrder); } } @@ -596,13 +558,14 @@ namespace Umbraco.Core.Services public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, string filter = "") { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var filterQuery = filter.IsNullOrWhiteSpace() ? null : repository.QueryT.Where(x => x.Name.Contains(filter)); + // fixme nesting uow?! return GetPagedChildren(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filterQuery); } } @@ -622,10 +585,10 @@ namespace Umbraco.Core.Services public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter) { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); + if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); + if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); @@ -634,9 +597,7 @@ namespace Umbraco.Core.Services //if the id is System Root, then just get all - NO! does not make sense! //if (id != Constants.System.Root) query.Where(x => x.ParentId == id); - var children = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); - uow.Complete(); - return children; + return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); } } @@ -653,13 +614,14 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var filterQuery = filter.IsNullOrWhiteSpace() ? null : repository.QueryT.Where(x => x.Name.Contains(filter)); + // fixme nesting uow? return GetPagedDescendants(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filterQuery); } } @@ -678,10 +640,10 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter) { - Mandate.ParameterCondition(pageIndex >= 0, nameof(pageIndex)); - Mandate.ParameterCondition(pageSize > 0, nameof(pageSize)); + if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); + if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); @@ -690,9 +652,7 @@ namespace Umbraco.Core.Services //if the id is System Root, then just get all if (id != Constants.System.Root) query.Where(x => x.Path.SqlContains($",{id},", TextColumnType.NVarchar)); - var descendants = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); - uow.Complete(); - return descendants; + return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); } } @@ -704,14 +664,12 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetChildrenByName(int parentId, string name) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.QueryT.Where(x => x.ParentId == parentId && x.Name.Contains(name)); - var children = repository.GetByQuery(query); - uow.Complete(); - return children; + return repository.GetByQuery(query); } } @@ -722,7 +680,7 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetDescendants(int id) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); @@ -734,9 +692,7 @@ namespace Umbraco.Core.Services } var pathMatch = content.Path + ","; var query = repository.QueryT.Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch)); - var descendants = repository.GetByQuery(query); - uow.Complete(); - return descendants; + return repository.GetByQuery(query); } } @@ -747,15 +703,13 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetDescendants(IContent content) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var pathMatch = content.Path + ","; var query = repository.QueryT.Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch)); - var descendants = repository.GetByQuery(query); - uow.Complete(); - return descendants; + return repository.GetByQuery(query); } } @@ -814,14 +768,12 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetRootContent() { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.QueryT.Where(x => x.ParentId == Constants.System.Root); - var items = repository.GetByQuery(query); - uow.Complete(); - return items; + return repository.GetByQuery(query); } } @@ -831,13 +783,11 @@ namespace Umbraco.Core.Services /// internal IEnumerable GetAllPublished() { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); - var content = repository.GetByPublishedVersion(QueryNotTrashed); - uow.Complete(); - return content; + return repository.GetByPublishedVersion(QueryNotTrashed); } } @@ -847,12 +797,10 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetContentForExpiration() { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); - var content = GetContentForExpiration(uow); - uow.Complete(); - return content; + return GetContentForExpiration(uow); } } @@ -869,12 +817,10 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetContentForRelease() { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); - var content = GetContentForRelease(uow); - uow.Complete(); - return content; + return GetContentForRelease(uow); } } @@ -891,15 +837,13 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetContentInRecycleBin() { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var bin = $"{Constants.System.Root},{Constants.System.RecycleBinContent},"; var query = repository.QueryT.Where(x => x.Path.StartsWith(bin)); - var content = repository.GetByQuery(query); - uow.Complete(); - return content; + return repository.GetByQuery(query); } } @@ -920,13 +864,12 @@ namespace Umbraco.Core.Services /// True if the content has any published version otherwise False public bool HasPublishedVersion(int id) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.QueryT.Where(x => x.Published && x.Id == id && x.Trashed == false); var count = repository.Count(query); - uow.Complete(); return count > 0; } } @@ -943,28 +886,24 @@ namespace Umbraco.Core.Services if (content.Trashed) return false; // trashed content is never publishable // not trashed and has a parent: publishable if the parent is path-published - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repo = uow.CreateRepository(); var parent = repo.Get(content.ParentId); if (parent == null) throw new Exception("Out of sync."); // causes rollback - var isPublishable = repo.IsPathPublished(parent); - uow.Complete(); - return isPublishable; + return repo.IsPathPublished(parent); } } public bool IsPathPublished(IContent content) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repo = uow.CreateRepository(); - var isPathPublished = repo.IsPathPublished(content); - uow.Complete(); - return isPathPublished; + return repo.IsPathPublished(content); } } @@ -993,13 +932,16 @@ namespace Umbraco.Core.Services { var evtMsgs = EventMessagesFactory.Get(); - if (raiseEvents && Saving.IsRaisedEventCancelled(new SaveEventArgs(content, evtMsgs), this)) - return OperationStatus.Attempt.Cancel(evtMsgs); - - var isNew = content.IsNewEntity(); - using (var uow = UowProvider.CreateUnitOfWork()) { + if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(content, evtMsgs))) + { + uow.Complete(); + return OperationStatus.Attempt.Cancel(evtMsgs); + } + + var isNew = content.IsNewEntity(); + uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); @@ -1014,15 +956,15 @@ namespace Umbraco.Core.Services repository.AddOrUpdate(content); + if (raiseEvents) + uow.Events.Dispatch(Saved, this, new SaveEventArgs(content, false, evtMsgs)); + var changeType = isNew ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode; + uow.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); + Audit(uow, AuditType.Save, "Save Content performed by user", userId, content.Id); + uow.Complete(); } - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(content, false, evtMsgs), this); - var changeType = isNew ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode; - TreeChanged.RaiseEvent(new TreeChange(content, changeType).ToEventArgs(), this); - Audit(AuditType.Save, "Save Content performed by user", userId, content.Id); - return OperationStatus.Attempt.Succeed(evtMsgs); } @@ -1052,14 +994,17 @@ namespace Umbraco.Core.Services var evtMsgs = EventMessagesFactory.Get(); var contentsA = contents.ToArray(); - if (raiseEvents && Saving.IsRaisedEventCancelled(new SaveEventArgs(contentsA, evtMsgs), this)) - return OperationStatus.Attempt.Cancel(evtMsgs); - - var treeChanges = contentsA.Select(x => new TreeChange(x, - x.IsNewEntity() ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode)); - using (var uow = UowProvider.CreateUnitOfWork()) { + if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(contentsA, evtMsgs))) + { + uow.Complete(); + return OperationStatus.Attempt.Cancel(evtMsgs); + } + + var treeChanges = contentsA.Select(x => new TreeChange(x, + x.IsNewEntity() ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode)); + uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); foreach (var content in contentsA) @@ -1076,14 +1021,14 @@ namespace Umbraco.Core.Services repository.AddOrUpdate(content); } + if (raiseEvents) + uow.Events.Dispatch(Saved, this, new SaveEventArgs(contentsA, false, evtMsgs)); + uow.Events.Dispatch(TreeChanged, this, treeChanges.ToEventArgs()); + Audit(uow, AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root); + uow.Complete(); } - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(contentsA, false, evtMsgs), this); - TreeChanged.RaiseEvent(treeChanges.ToEventArgs(), this); - Audit(AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root); - return OperationStatus.Attempt.Succeed(evtMsgs); } @@ -1145,7 +1090,7 @@ namespace Umbraco.Core.Services /// True if unpublishing succeeded, otherwise False public bool UnPublish(IContent content, int userId = 0) { - return ((IContentServiceOperations)this).UnPublish(content, userId).Success; + return ((IContentServiceOperations) this).UnPublish(content, userId).Success; } /// @@ -1168,7 +1113,7 @@ namespace Umbraco.Core.Services /// True if publishing succeeded, otherwise False public Attempt SaveAndPublishWithStatus(IContent content, int userId = 0, bool raiseEvents = true) { - return ((IContentServiceOperations)this).SaveAndPublish(content, userId, raiseEvents); + return ((IContentServiceOperations) this).SaveAndPublish(content, userId, raiseEvents); } /// @@ -1213,7 +1158,7 @@ namespace Umbraco.Core.Services /// True if publishing succeeded, otherwise False public IEnumerable> PublishWithChildrenWithStatus(IContent content, int userId = 0, bool includeUnpublished = false) { - return ((IContentServiceOperations)this).PublishWithChildren(content, userId, includeUnpublished); + return ((IContentServiceOperations) this).PublishWithChildren(content, userId, includeUnpublished); } /// @@ -1303,11 +1248,14 @@ namespace Umbraco.Core.Services { var evtMsgs = EventMessagesFactory.Get(); - if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(content, evtMsgs), this)) - return OperationStatus.Attempt.Cancel(evtMsgs); - using (var uow = UowProvider.CreateUnitOfWork()) { + if (uow.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(content, evtMsgs))) + { + uow.Complete(); + return OperationStatus.Attempt.Cancel(evtMsgs); + } + uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); @@ -1315,19 +1263,20 @@ namespace Umbraco.Core.Services // but... UnPublishing event makes no sense (not going to cancel?) and no need to save // just raise the event if (content.Trashed == false && content.HasPublishedVersion) - UnPublished.RaiseEvent(new PublishEventArgs(content, false, false), this); + uow.Events.Dispatch(UnPublished, this, new PublishEventArgs(content, false, false)); + + DeleteLocked(uow, repository, content); + + uow.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.Remove).ToEventArgs()); + Audit(uow, AuditType.Delete, "Delete Content performed by user", userId, content.Id); - DeleteLocked(repository, content); uow.Complete(); - TreeChanged.RaiseEvent(new TreeChange(content, TreeChangeTypes.Remove).ToEventArgs(), this); } - Audit(AuditType.Delete, "Delete Content performed by user", userId, content.Id); - return OperationStatus.Attempt.Succeed(evtMsgs); } - private void DeleteLocked(IContentRepository repository, IContent content) + private void DeleteLocked(IScopeUnitOfWork uow, IContentRepository repository, IContent content) { // then recursively delete descendants, bottom-up // just repository.Delete + an event @@ -1350,8 +1299,9 @@ namespace Umbraco.Core.Services repository.Delete(c); var args = new DeleteEventArgs(c, false); // raise event & get flagged files - Deleted.RaiseEvent(args, this); + uow.Events.Dispatch(Deleted, this, args); + // fixme not going to work, do it differently _mediaFileSystem.DeleteFiles(args.MediaFilesToDelete, // remove flagged files (file, e) => Logger.Error("An error occurred while deleting file attached to nodes: " + file, e)); } @@ -1372,20 +1322,23 @@ namespace Umbraco.Core.Services /// Optional Id of the User deleting versions of a Content object public void DeleteVersions(int id, DateTime versionDate, int userId = 0) { - if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, dateToRetain: versionDate), this)) - return; - using (var uow = UowProvider.CreateUnitOfWork()) { + if (uow.Events.DispatchCancelable(DeletingVersions, this, new DeleteRevisionsEventArgs(id, dateToRetain: versionDate))) + { + uow.Complete(); + return; + } + uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); repository.DeleteVersions(id, versionDate); + + uow.Events.Dispatch(DeletedVersions, this, new DeleteRevisionsEventArgs(id, false, dateToRetain: versionDate)); + Audit(uow, AuditType.Delete, "Delete Content by version date performed by user", userId, Constants.System.Root); + uow.Complete(); } - - DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false, dateToRetain: versionDate), this); - - Audit(AuditType.Delete, "Delete Content by version date performed by user", userId, Constants.System.Root); } /// @@ -1398,26 +1351,30 @@ namespace Umbraco.Core.Services /// Optional Id of the User deleting versions of a Content object public void DeleteVersion(int id, Guid versionId, bool deletePriorVersions, int userId = 0) { - if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, /*specificVersion:*/ versionId), this)) - return; - - if (deletePriorVersions) - { - var content = GetByVersion(versionId); - DeleteVersions(id, content.UpdateDate, userId); - } - using (var uow = UowProvider.CreateUnitOfWork()) { + if (uow.Events.DispatchCancelable(DeletingVersions, this, new DeleteRevisionsEventArgs(id, /*specificVersion:*/ versionId))) + { + uow.Complete(); + return; + } + + if (deletePriorVersions) + { + var content = GetByVersion(versionId); + // fixme nesting uow? + DeleteVersions(id, content.UpdateDate, userId); + } + uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); repository.DeleteVersion(versionId); + + uow.Events.Dispatch(DeletedVersions, this, new DeleteRevisionsEventArgs(id, false,/* specificVersion:*/ versionId)); + Audit(uow, AuditType.Delete, "Delete Content by version performed by user", userId, Constants.System.Root); + uow.Complete(); } - - DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false,/* specificVersion:*/ versionId), this); - - Audit(AuditType.Delete, "Delete Content by version performed by user", userId, Constants.System.Root); } #endregion @@ -1452,8 +1409,11 @@ namespace Umbraco.Core.Services var repository = uow.CreateRepository(); var originalPath = content.Path; - if (Trashing.IsRaisedEventCancelled(new MoveEventArgs(new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent)), this)) + if (uow.Events.DispatchCancelable(Trashing, this, new MoveEventArgs(new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent)))) + { + uow.Complete(); return OperationStatus.Attempt.Cancel(evtMsgs); // causes rollback + } // if it's published we may want to force-unpublish it - that would be backward-compatible... but... // making a radical decision here: trashing is equivalent to moving under an unpublished node so @@ -1462,17 +1422,18 @@ namespace Umbraco.Core.Services //{ } PerformMoveLocked(repository, content, Constants.System.RecycleBinContent, null, userId, moves, true); + uow.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs()); + + var moveInfo = moves + .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) + .ToArray(); + + uow.Events.Dispatch(Trashed, this, new MoveEventArgs(false, evtMsgs, moveInfo)); + Audit(uow, AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); + uow.Complete(); - TreeChanged.RaiseEvent(new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs(), this); } - var moveInfo = moves - .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) - .ToArray(); - - Trashed.RaiseEvent(new MoveEventArgs(false, evtMsgs, moveInfo), this); - Audit(AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); - return OperationStatus.Attempt.Succeed(evtMsgs); } @@ -1507,8 +1468,11 @@ namespace Umbraco.Core.Services if (parentId != Constants.System.Root && (parent == null || parent.Trashed)) throw new InvalidOperationException("Parent does not exist or is trashed."); // causes rollback - if (Moving.IsRaisedEventCancelled(new MoveEventArgs(new MoveEventInfo(content, content.Path, parentId)), this)) + if (uow.Events.DispatchCancelable(Moving, this, new MoveEventArgs(new MoveEventInfo(content, content.Path, parentId)))) + { + uow.Complete(); return; // causes rollback + } // if content was trashed, and since we're not moving to the recycle bin, // indicate that the trashed status should be changed to false, else just @@ -1527,17 +1491,17 @@ namespace Umbraco.Core.Services PerformMoveLocked(repository, content, parentId, parent, userId, moves, trashed); + uow.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs()); + + var moveInfo = moves //changes + .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) + .ToArray(); + + uow.Events.Dispatch(Moved, this, new MoveEventArgs(false, moveInfo)); + Audit(uow, AuditType.Move, "Move Content performed by user", userId, content.Id); + uow.Complete(); - TreeChanged.RaiseEvent(new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs(), this); } - - var moveInfo = moves //changes - .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) - .ToArray(); - - Moved.RaiseEvent(new MoveEventArgs(false, moveInfo), this); - - Audit(AuditType.Move, "Move Content performed by user", userId, content.Id); } // MUST be called from within WriteLock @@ -1609,24 +1573,27 @@ namespace Umbraco.Core.Services // are managed by Delete, and not here. // no idea what those events are for, keep a simplified version - if (EmptyingRecycleBin.IsRaisedEventCancelled(new RecycleBinEventArgs(nodeObjectType), this)) + if (uow.Events.DispatchCancelable(EmptyingRecycleBin, this, new RecycleBinEventArgs(nodeObjectType))) + { + uow.Complete(); return; // causes rollback + } // emptying the recycle bin means deleting whetever is in there - do it properly! var query = repository.QueryT.Where(x => x.ParentId == Constants.System.RecycleBinContent); var contents = repository.GetByQuery(query).ToArray(); foreach (var content in contents) { - DeleteLocked(repository, content); + DeleteLocked(uow, repository, content); deleted.Add(content); } - EmptiedRecycleBin.RaiseEvent(new RecycleBinEventArgs(nodeObjectType, true), this); - uow.Complete(); - TreeChanged.RaiseEvent(deleted.Select(x => new TreeChange(x, TreeChangeTypes.Remove)).ToEventArgs(), this); - } + uow.Events.Dispatch(EmptiedRecycleBin, this, new RecycleBinEventArgs(nodeObjectType, true)); + uow.Events.Dispatch(TreeChanged, this, deleted.Select(x => new TreeChange(x, TreeChangeTypes.Remove)).ToEventArgs()); + Audit(uow, AuditType.Delete, "Empty Content Recycle Bin performed by user", 0, Constants.System.RecycleBinContent); - Audit(AuditType.Delete, "Empty Content Recycle Bin performed by user", 0, Constants.System.RecycleBinContent); + uow.Complete(); + } } #endregion @@ -1662,17 +1629,20 @@ namespace Umbraco.Core.Services var copy = content.DeepCloneWithResetIdentities(); copy.ParentId = parentId; - if (Copying.IsRaisedEventCancelled(new CopyEventArgs(content, copy, parentId), this)) - return null; - - // note - relateToOriginal is not managed here, - // it's just part of the Copied event args so the RelateOnCopyHandler knows what to do - // meaning that the event has to trigger for every copied content including descendants - - var copies = new List>(); - using (var uow = UowProvider.CreateUnitOfWork()) { + if (uow.Events.DispatchCancelable(Copying, this, new CopyEventArgs(content, copy, parentId))) + { + uow.Complete(); + return null; + } + + // note - relateToOriginal is not managed here, + // it's just part of the Copied event args so the RelateOnCopyHandler knows what to do + // meaning that the event has to trigger for every copied content including descendants + + var copies = new List>(); + uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); @@ -1701,7 +1671,7 @@ namespace Umbraco.Core.Services var descendantCopy = descendant.DeepCloneWithResetIdentities(); descendantCopy.ParentId = parentId; - if (Copying.IsRaisedEventCancelled(new CopyEventArgs(descendant, descendantCopy, parentId), this)) + if (uow.Events.DispatchCancelable(Copying, this, new CopyEventArgs(descendant, descendantCopy, parentId))) continue; // a copy is .Saving and will be .Unpublished @@ -1724,13 +1694,14 @@ namespace Umbraco.Core.Services // - tags should be handled by the content repository // - a copy is unpublished and therefore has no impact on tags in DB + uow.Events.Dispatch(TreeChanged, this, new TreeChange(copy, TreeChangeTypes.RefreshBranch).ToEventArgs()); + foreach (var x in copies) + uow.Events.Dispatch(Copied, this, new CopyEventArgs(x.Item1, x.Item2, false, x.Item2.ParentId, relateToOriginal)); + Audit(uow, AuditType.Copy, "Copy Content performed by user", content.WriterId, content.Id); + uow.Complete(); } - TreeChanged.RaiseEvent(new TreeChange(copy, TreeChangeTypes.RefreshBranch).ToEventArgs(), this); - foreach (var x in copies) - Copied.RaiseEvent(new CopyEventArgs(x.Item1, x.Item2, false, x.Item2.ParentId, relateToOriginal), this); - Audit(AuditType.Copy, "Copy Content performed by user", content.WriterId, content.Id); return copy; } @@ -1742,15 +1713,21 @@ namespace Umbraco.Core.Services /// True if sending publication was succesfull otherwise false public bool SendToPublication(IContent content, int userId = 0) { - if (SendingToPublish.IsRaisedEventCancelled(new SendToPublishEventArgs(content), this)) - return false; + using (var uow = UowProvider.CreateUnitOfWork()) + { + if (uow.Events.DispatchCancelable(SendingToPublish, this, new SendToPublishEventArgs(content))) + { + uow.Complete(); + return false; + } - //Save before raising event - Save(content, userId); + //Save before raising event + // fixme - nesting uow? + Save(content, userId); - SentToPublish.RaiseEvent(new SendToPublishEventArgs(content, false), this); - - Audit(AuditType.SendToPublish, "Send to Publish performed by user", content.WriterId, content.Id); + uow.Events.Dispatch(SentToPublish, this, new SendToPublishEventArgs(content, false)); + Audit(uow, AuditType.SendToPublish, "Send to Publish performed by user", content.WriterId, content.Id); + } return true; } @@ -1771,19 +1748,22 @@ namespace Umbraco.Core.Services { var content = GetByVersion(versionId); - if (RollingBack.IsRaisedEventCancelled(new RollbackEventArgs(content), this)) - return content; - - content.CreatorId = userId; - - // need to make sure that the repository is going to save a new version - // but if we're not changing anything, the repository would not save anything - // so - make sure the property IS dirty, doing a flip-flop with an impossible value - content.WriterId = -1; - content.WriterId = userId; - using (var uow = UowProvider.CreateUnitOfWork()) { + if (uow.Events.DispatchCancelable(RollingBack, this, new RollbackEventArgs(content))) + { + uow.Complete(); + return content; + } + + content.CreatorId = userId; + + // need to make sure that the repository is going to save a new version + // but if we're not changing anything, the repository would not save anything + // so - make sure the property IS dirty, doing a flip-flop with an impossible value + content.WriterId = -1; + content.WriterId = userId; + uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); @@ -1791,14 +1771,14 @@ namespace Umbraco.Core.Services content.ChangePublishedState(PublishedState.Saving); repository.AddOrUpdate(content); + + uow.Events.Dispatch(RolledBack, this, new RollbackEventArgs(content, false)); + uow.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshNode).ToEventArgs()); + Audit(uow, AuditType.RollBack, "Content rollback performed by user", content.WriterId, content.Id); + uow.Complete(); } - RolledBack.RaiseEvent(new RollbackEventArgs(content, false), this); - TreeChanged.RaiseEvent(new TreeChange(content, TreeChangeTypes.RefreshNode).ToEventArgs(), this); - - Audit(AuditType.RollBack, "Content rollback performed by user", content.WriterId, content.Id); - return content; } @@ -1819,14 +1799,14 @@ namespace Umbraco.Core.Services var itemsA = items.ToArray(); if (itemsA.Length == 0) return true; - if (raiseEvents && Saving.IsRaisedEventCancelled(new SaveEventArgs(itemsA), this)) - return false; - - var published = new List(); - var saved = new List(); - using (var uow = UowProvider.CreateUnitOfWork()) { + if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(itemsA))) + return false; + + var published = new List(); + var saved = new List(); + uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var sortOrder = 0; @@ -1857,19 +1837,19 @@ namespace Umbraco.Core.Services repository.AddOrUpdate(content); } + if (raiseEvents) + uow.Events.Dispatch(Saved, this, new SaveEventArgs(saved, false)); + + if (raiseEvents && published.Any()) + uow.Events.Dispatch(Published, this, new PublishEventArgs(published, false, false)); + + uow.Events.Dispatch(TreeChanged, this, saved.Select(x => new TreeChange(x, TreeChangeTypes.RefreshNode)).ToEventArgs()); + + Audit(uow, AuditType.Sort, "Sorting content performed by user", userId, 0); + uow.Complete(); } - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(saved, false), this); - - if (raiseEvents && published.Any()) - Published.RaiseEvent(new PublishEventArgs(published, false, false), this); - - TreeChanged.RaiseEvent(saved.Select(x => new TreeChange(x, TreeChangeTypes.RefreshNode)).ToEventArgs(), this); - - Audit(AuditType.Sort, "Sorting content performed by user", userId, 0); - return true; } @@ -1884,13 +1864,11 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects internal IEnumerable GetPublishedDescendants(IContent content) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); - var descendants = GetPublishedDescendantsLocked(repository, content); - uow.Complete(); - return descendants; + return GetPublishedDescendantsLocked(repository, content); } } @@ -1919,14 +1897,10 @@ namespace Umbraco.Core.Services #region Private Methods - private void Audit(AuditType type, string message, int userId, int objectId) + private void Audit(IUnitOfWork uow, AuditType type, string message, int userId, int objectId) { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - repo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); - uow.Complete(); - } + var repo = uow.CreateRepository(); + repo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); } /// @@ -1954,7 +1928,7 @@ namespace Umbraco.Core.Services var repository = uow.CreateRepository(); // fail fast + use in alreadyChecked below to avoid duplicate checks - var attempt = StrategyCanPublish(content, userId, /*checkPath:*/ true, evtMsgs); + var attempt = StrategyCanPublish(uow, content, userId, /*checkPath:*/ true, evtMsgs); if (attempt.Success == false) return new[] { attempt }; // causes rollback @@ -1966,7 +1940,7 @@ namespace Umbraco.Core.Services // - published w/changes: publish those changes // - unpublished: publish if includeUnpublished, otherwise ignore var alreadyChecked = new[] { content }; - attempts = StrategyPublishWithChildren(contents, alreadyChecked, userId, evtMsgs, includeUnpublished).ToArray(); + attempts = StrategyPublishWithChildren(uow, contents, alreadyChecked, userId, evtMsgs, includeUnpublished).ToArray(); foreach (var status in attempts.Where(x => x.Success).Select(x => x.Result)) { @@ -1978,12 +1952,13 @@ namespace Umbraco.Core.Services publishedItems.Add(publishedItem); } + uow.Events.Dispatch(Published, this, new PublishEventArgs(publishedItems, false, false)); + uow.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs()); + Audit(uow, AuditType.Publish, "Publish with Children performed by user", userId, content.Id); + uow.Complete(); } - Published.RaiseEvent(new PublishEventArgs(publishedItems, false, false), this); - TreeChanged.RaiseEvent(new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs(), this); - Audit(AuditType.Publish, "Publish with Children performed by user", userId, content.Id); return attempts; } @@ -2013,20 +1988,22 @@ namespace Umbraco.Core.Services } // strategy - var attempt = StrategyCanUnPublish(content, userId, evtMsgs); + // fixme should we still complete the uow? don't want to rollback here! + var attempt = StrategyCanUnPublish(uow, content, userId, evtMsgs); if (attempt == false) return attempt; // causes rollback - attempt = StrategyUnPublish(content, true, userId, evtMsgs); + attempt = StrategyUnPublish(uow, content, true, userId, evtMsgs); if (attempt == false) return attempt; // causes rollback content.WriterId = userId; repository.AddOrUpdate(content); + uow.Events.Dispatch(UnPublished, this, new PublishEventArgs(content, false, false)); + uow.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs()); + Audit(uow, AuditType.UnPublish, "UnPublish performed by user", userId, content.Id); + uow.Complete(); } - UnPublished.RaiseEvent(new PublishEventArgs(content, false, false), this); - TreeChanged.RaiseEvent(new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs(), this); - Audit(AuditType.UnPublish, "UnPublish performed by user", userId, content.Id); return Attempt.Succeed(new UnPublishStatus(UnPublishedStatusType.Success, evtMsgs, content)); } @@ -2040,26 +2017,29 @@ namespace Umbraco.Core.Services private Attempt SaveAndPublishDo(IContent content, int userId = 0, bool raiseEvents = true) { var evtMsgs = EventMessagesFactory.Get(); - - if (raiseEvents && Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) - return Attempt.Fail(new PublishStatus(PublishStatusType.FailedCancelledByEvent, evtMsgs, content)); - - var isNew = content.IsNewEntity(); - var changeType = isNew ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode; - var previouslyPublished = content.HasIdentity && content.HasPublishedVersion; Attempt status; using (var uow = UowProvider.CreateUnitOfWork()) { + if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(content))) + { + uow.Complete(); + return Attempt.Fail(new PublishStatus(PublishStatusType.FailedCancelledByEvent, evtMsgs, content)); + } + + var isNew = content.IsNewEntity(); + var changeType = isNew ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode; + var previouslyPublished = content.HasIdentity && content.HasPublishedVersion; + uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); - status = StrategyCanPublish(content, userId, /*checkPath:*/ true, evtMsgs); + status = StrategyCanPublish(uow, content, userId, /*checkPath:*/ true, evtMsgs); if (status.Success) { // strategy handles events, and various business rules eg release & expire // dates, trashed status... - status = StrategyPublish(content, true, userId, evtMsgs); + status = StrategyPublish(uow, content, true, userId, evtMsgs); } // save - always, even if not publishing (this is SaveAndPublish) @@ -2069,37 +2049,38 @@ namespace Umbraco.Core.Services repository.AddOrUpdate(content); + if (raiseEvents) // always + uow.Events.Dispatch(Saved, this, new SaveEventArgs(content, false, evtMsgs)); + + if (status.Success == false) + { + uow.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); + return status; + } + + uow.Events.Dispatch(Published, this, new PublishEventArgs(content, false, false)); + + // if was not published and now is... descendants that were 'published' (but + // had an unpublished ancestor) are 're-published' ie not explicitely published + // but back as 'published' nevertheless + if (isNew == false && previouslyPublished == false) + { + if (HasChildren(content.Id)) + { + var descendants = GetPublishedDescendants(content).ToArray(); + uow.Events.Dispatch(Published, this, new PublishEventArgs(descendants, false, false)); + } + changeType = TreeChangeTypes.RefreshBranch; // whole branch + } + + // invalidate the node/branch + uow.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); + + Audit(uow, AuditType.Publish, "Save and Publish performed by user", userId, content.Id); + uow.Complete(); } - if (raiseEvents) // always - Saved.RaiseEvent(new SaveEventArgs(content, false, evtMsgs), this); - - if (status.Success == false) - { - TreeChanged.RaiseEvent(new TreeChange(content, changeType).ToEventArgs(), this); - return status; - } - - Published.RaiseEvent(new PublishEventArgs(content, false, false), this); - - // if was not published and now is... descendants that were 'published' (but - // had an unpublished ancestor) are 're-published' ie not explicitely published - // but back as 'published' nevertheless - if (isNew == false && previouslyPublished == false) - { - if (HasChildren(content.Id)) - { - var descendants = GetPublishedDescendants(content).ToArray(); - Published.RaiseEvent(new PublishEventArgs(descendants, false, false), this); - } - changeType = TreeChangeTypes.RefreshBranch; // whole branch - } - - // invalidate the node/branch - TreeChanged.RaiseEvent(new TreeChange(content, changeType).ToEventArgs(), this); - - Audit(AuditType.Publish, "Save and Publish performed by user", userId, content.Id); return status; } @@ -2243,9 +2224,9 @@ namespace Umbraco.Core.Services // prob. want to find nicer names? - internal Attempt StrategyCanPublish(IContent content, int userId, bool checkPath, EventMessages evtMsgs) + internal Attempt StrategyCanPublish(IScopeUnitOfWork uow, IContent content, int userId, bool checkPath, EventMessages evtMsgs) { - if (Publishing.IsRaisedEventCancelled(new PublishEventArgs(content, evtMsgs), this)) + if (uow.Events.DispatchCancelable(Publishing, this, new PublishEventArgs(content, evtMsgs))) { Logger.Info($"Content '{content.Name}' with Id '{content.Id}' will not be published, the event was cancelled."); return Attempt.Fail(new PublishStatus(PublishStatusType.FailedCancelledByEvent, evtMsgs, content)); @@ -2318,7 +2299,7 @@ namespace Umbraco.Core.Services return Attempt.Succeed(new PublishStatus(content, evtMsgs)); } - internal Attempt StrategyPublish(IContent content, bool alreadyCheckedCanPublish, int userId, EventMessages evtMsgs) + internal Attempt StrategyPublish(IScopeUnitOfWork uow, IContent content, bool alreadyCheckedCanPublish, int userId, EventMessages evtMsgs) { // note: when used at top-level, StrategyCanPublish with checkPath=true should have run already // and alreadyCheckedCanPublish should be true, so not checking again. when used at nested level, @@ -2326,7 +2307,7 @@ namespace Umbraco.Core.Services var attempt = alreadyCheckedCanPublish ? Attempt.Succeed(new PublishStatus(content, evtMsgs)) // already know we can - : StrategyCanPublish(content, userId, /*checkPath:*/ false, evtMsgs); // else check + : StrategyCanPublish(uow, content, userId, /*checkPath:*/ false, evtMsgs); // else check if (attempt.Success == false) return attempt; @@ -2365,7 +2346,7 @@ namespace Umbraco.Core.Services /// - if includeUnpublished is true, process the underlying branch /// - else, do not process the underlying branch /// - internal IEnumerable> StrategyPublishWithChildren(IEnumerable contents, IEnumerable alreadyChecked, int userId, EventMessages evtMsgs, bool includeUnpublished = true) + internal IEnumerable> StrategyPublishWithChildren(IScopeUnitOfWork uow, IEnumerable contents, IEnumerable alreadyChecked, int userId, EventMessages evtMsgs, bool includeUnpublished = true) { var statuses = new List>(); var alreadyCheckedA = (alreadyChecked ?? Enumerable.Empty()).ToArray(); @@ -2400,7 +2381,7 @@ namespace Umbraco.Core.Services { // newest is published already but we are topLevel, or // newest is not published, but another version is - publish newest - var r = StrategyPublish(content, alreadyCheckedA.Contains(content), userId, evtMsgs); + var r = StrategyPublish(uow, content, alreadyCheckedA.Contains(content), userId, evtMsgs); if (r.Success == false) { // we tried to publish and it failed, but it already had / still has a published version, @@ -2420,7 +2401,7 @@ namespace Umbraco.Core.Services // because it is top-level or because we include unpublished. // if publishing fails, and because content does not have a published // version at all, ensure we do not process its descendants - var r = StrategyPublish(content, alreadyCheckedA.Contains(content), userId, evtMsgs); + var r = StrategyPublish(uow, content, alreadyCheckedA.Contains(content), userId, evtMsgs); if (r.Success == false) excude.Add(content.Id); @@ -2436,10 +2417,10 @@ namespace Umbraco.Core.Services return statuses; } - internal Attempt StrategyCanUnPublish(IContent content, int userId, EventMessages evtMsgs) + internal Attempt StrategyCanUnPublish(IScopeUnitOfWork uow, IContent content, int userId, EventMessages evtMsgs) { // fire UnPublishing event - if (UnPublishing.IsRaisedEventCancelled(new PublishEventArgs(content, evtMsgs), this)) + if (uow.Events.DispatchCancelable(UnPublishing, this, new PublishEventArgs(content, evtMsgs))) { Logger.Info($"Content '{content.Name}' with Id '{content.Id}' will not be unpublished, the event was cancelled."); return Attempt.Fail(new UnPublishStatus(UnPublishedStatusType.FailedCancelledByEvent, evtMsgs, content)); @@ -2448,14 +2429,14 @@ namespace Umbraco.Core.Services return Attempt.Succeed(new UnPublishStatus(content, evtMsgs)); } - internal Attempt StrategyUnPublish(IContent content, bool alreadyCheckedCanUnPublish, int userId, EventMessages evtMsgs) + internal Attempt StrategyUnPublish(IScopeUnitOfWork uow, IContent content, bool alreadyCheckedCanUnPublish, int userId, EventMessages evtMsgs) { // content should (is assumed to) be the newest version, which may not be published, // don't know how to test this, so it's not verified var attempt = alreadyCheckedCanUnPublish ? Attempt.Succeed(new UnPublishStatus(content, evtMsgs)) // already know we can - : StrategyCanUnPublish(content, userId, evtMsgs); + : StrategyCanUnPublish(uow, content, userId, evtMsgs); if (attempt.Success == false) return attempt; @@ -2476,9 +2457,9 @@ namespace Umbraco.Core.Services return attempt; } - internal IEnumerable> StrategyUnPublish(IEnumerable content, int userId, EventMessages evtMsgs) + internal IEnumerable> StrategyUnPublish(IScopeUnitOfWork uow, IEnumerable content, int userId, EventMessages evtMsgs) { - return content.Select(x => StrategyUnPublish(x, false, userId, evtMsgs)); + return content.Select(x => StrategyUnPublish(uow, x, false, userId, evtMsgs)); } #endregion @@ -2495,7 +2476,7 @@ namespace Umbraco.Core.Services /// /// Id of the /// Optional Id of the user issueing the delete operation - public void DeleteContentOfType(int contentTypeId, int userId = 0) + public void DeleteContentOfTypes(IEnumerable contentTypeIds, int userId = 0) { //TODO: This currently this is called from the ContentTypeService but that needs to change, // if we are deleting a content type, we should just delete the data and do this operation slightly differently. @@ -2506,17 +2487,21 @@ namespace Umbraco.Core.Services var changes = new List>(); var moves = new List>(); + var contentTypeIdsA = contentTypeIds.ToArray(); using (var uow = UowProvider.CreateUnitOfWork()) { uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); - var query = repository.QueryT.Where(x => x.ContentTypeId == contentTypeId); + var query = repository.QueryT.WhereIn(x => x.ContentTypeId, contentTypeIdsA); var contents = repository.GetByQuery(query).ToArray(); - if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(contents), this)) - return; // causes rollback + if (uow.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(contents))) + { + uow.Complete(); + return; + } // order by level, descending, so deepest first - that way, we cannot move // a content of the deleted type, to the recycle bin (and then delete it...) @@ -2526,13 +2511,13 @@ namespace Umbraco.Core.Services // but... UnPublishing event makes no sense (not going to cancel?) and no need to save // just raise the event if (content.Trashed == false && content.HasPublishedVersion) - UnPublished.RaiseEvent(new PublishEventArgs(content, false, false), this); + uow.Events.Dispatch(UnPublished, this, new PublishEventArgs(content, false, false)); // if current content has children, move them to trash var c = content; var childQuery = repository.QueryT.Where(x => x.Path.StartsWith(c.Path)); var children = repository.GetByQuery(childQuery); - foreach (var child in children.Where(x => x.ContentTypeId != contentTypeId)) + foreach (var child in children.Where(x => contentTypeIdsA.Contains(x.ContentTypeId) == false)) { // see MoveToRecycleBin PerformMoveLocked(repository, child, Constants.System.RecycleBinContent, null, userId, moves, true); @@ -2541,40 +2526,58 @@ namespace Umbraco.Core.Services // delete content // triggers the deleted event (and handles the files) - DeleteLocked(repository, content); + DeleteLocked(uow, repository, content); changes.Add(new TreeChange(content, TreeChangeTypes.Remove)); } + var moveInfos = moves + .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) + .ToArray(); + if (moveInfos.Length > 0) + uow.Events.Dispatch(Trashed, this, new MoveEventArgs(false, moveInfos)); + uow.Events.Dispatch(TreeChanged, this, changes.ToEventArgs()); + + Audit(uow, AuditType.Delete, $"Delete Content of Type {string.Join(",", contentTypeIdsA)} performed by user", userId, Constants.System.Root); + uow.Complete(); } + } - var moveInfos = moves - .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) - .ToArray(); - if (moveInfos.Length > 0) - Trashed.RaiseEvent(new MoveEventArgs(false, moveInfos), this); - TreeChanged.RaiseEvent(changes.ToEventArgs(), this); + /// + /// Deletes all content items of specified type. All children of deleted content item is moved to Recycle Bin. + /// + /// This needs extra care and attention as its potentially a dangerous and extensive operation + /// Id of the + /// Optional id of the user deleting the media + public void DeleteContentOfType(int contentTypeId, int userId = 0) + { + DeleteContentOfTypes(new[] { contentTypeId }, userId); + } - Audit(AuditType.Delete, $"Delete Content of Type {contentTypeId} performed by user", userId, Constants.System.Root); + + private IContentType GetContentType(IScopeUnitOfWork uow, string contentTypeAlias) + { + if (string.IsNullOrWhiteSpace(contentTypeAlias)) throw new ArgumentNullOrEmptyException(nameof(contentTypeAlias)); + + uow.ReadLock(Constants.Locks.ContentTypes); + + var repository = uow.CreateRepository(); + var query = repository.QueryT.Where(x => x.Alias == contentTypeAlias); + var contentType = repository.GetByQuery(query).FirstOrDefault(); + + if (contentType == null) + throw new Exception($"No ContentType matching the passed in Alias: '{contentTypeAlias}' was found"); // causes rollback + + return contentType; } private IContentType GetContentType(string contentTypeAlias) { - Mandate.ParameterNotNullOrEmpty(contentTypeAlias, nameof(contentTypeAlias)); + if (string.IsNullOrWhiteSpace(contentTypeAlias)) throw new ArgumentNullOrEmptyException(nameof(contentTypeAlias)); - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { - uow.ReadLock(Constants.Locks.ContentTypes); - - var repository = uow.CreateRepository(); - var query = repository.QueryT.Where(x => x.Alias == contentTypeAlias); - var contentType = repository.GetByQuery(query).FirstOrDefault(); - - if (contentType == null) - throw new Exception($"No ContentType matching the passed in Alias: '{contentTypeAlias}' was found"); // causes rollback - - uow.Complete(); - return contentType; + return GetContentType(uow, contentTypeAlias); } } diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index 85a728340c..5c73ad5980 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -13,13 +13,13 @@ namespace Umbraco.Core.Services /// internal class ContentTypeService : ContentTypeServiceBase, IContentTypeService { - public ContentTypeService(IDatabaseUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, IContentService contentService) + public ContentTypeService(IScopeUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, IContentService contentService) : base(provider, logger, eventMessagesFactory) { ContentService = contentService; } - protected override IContentTypeService Instance => this; + protected override IContentTypeService This => this; // beware! order is important to avoid deadlocks protected override int[] ReadLockIds { get; } = { Constants.Locks.ContentTypes }; protected override int[] WriteLockIds { get; } = { Constants.Locks.ContentTree, Constants.Locks.ContentTypes }; @@ -41,16 +41,12 @@ namespace Umbraco.Core.Services /// Beware! Works accross content, media and member types. public IEnumerable GetAllPropertyTypeAliases() { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { // that one is special because it works accross content, media and member types uow.ReadLock(Constants.Locks.ContentTypes, Constants.Locks.MediaTypes, Constants.Locks.MemberTypes); var repo = uow.CreateRepository(); - var aliases = repo.GetAllPropertyTypeAliases(); - uow.Complete(); - return aliases; - - + return repo.GetAllPropertyTypeAliases(); } } @@ -62,14 +58,12 @@ namespace Umbraco.Core.Services /// Beware! Works accross content, media and member types. public IEnumerable GetAllContentTypeAliases(params Guid[] guids) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { // that one is special because it works accross content, media and member types uow.ReadLock(Constants.Locks.ContentTypes, Constants.Locks.MediaTypes, Constants.Locks.MemberTypes); var repo = uow.CreateRepository(); - var aliases = repo.GetAllContentTypeAliases(guids); - uow.Complete(); - return aliases; + return repo.GetAllContentTypeAliases(guids); } } } diff --git a/src/Umbraco.Core/Services/ContentTypeServiceBase.cs b/src/Umbraco.Core/Services/ContentTypeServiceBase.cs index c50dea2d62..28f7e9a65f 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceBase.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceBase.cs @@ -1,1031 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Core.Events; -using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Models.EntityBase; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; -using Umbraco.Core.Services.Changes; namespace Umbraco.Core.Services { - internal abstract class ContentTypeServiceBase : RepositoryService + internal abstract class ContentTypeServiceBase : ScopeRepositoryService { - protected ContentTypeServiceBase(IDatabaseUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory) + protected ContentTypeServiceBase(IScopeUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory) : base(provider, logger, eventMessagesFactory) { } } - - internal abstract class ContentTypeServiceBase : ContentTypeServiceBase - where TItem : class, IContentTypeComposition - where TService : class, IContentTypeServiceBase - { - protected ContentTypeServiceBase(IDatabaseUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory) - : base(provider, logger, eventMessagesFactory) - { } - - protected abstract TService Instance { get; } - - internal static event TypedEventHandler.EventArgs> Changed; - - protected void OnChanged(ContentTypeChange.EventArgs args) - { - Changed.RaiseEvent(args, Instance); - } - - public static event TypedEventHandler.EventArgs> UowRefreshedEntity; - - protected void OnUowRefreshedEntity(ContentTypeChange.EventArgs args) - { - UowRefreshedEntity.RaiseEvent(args, Instance); - } - - public static event TypedEventHandler> Saving; - public static event TypedEventHandler> Saved; - - protected void OnSaving(SaveEventArgs args) - { - Saving.RaiseEvent(args, Instance); - } - - protected bool OnSavingCancelled(SaveEventArgs args) - { - return Saving.IsRaisedEventCancelled(args, Instance); - } - - protected void OnSaved(SaveEventArgs args) - { - Saved.RaiseEvent(args, Instance); - } - - public static event TypedEventHandler> Deleting; - public static event TypedEventHandler> Deleted; - - protected void OnDeleting(DeleteEventArgs args) - { - Deleting.RaiseEvent(args, Instance); - } - - protected bool OnDeletingCancelled(DeleteEventArgs args) - { - return Deleting.IsRaisedEventCancelled(args, Instance); - } - - protected void OnDeleted(DeleteEventArgs args) - { - Deleted.RaiseEvent(args, (TService)(object)this); - } - - public static event TypedEventHandler> Moving; - public static event TypedEventHandler> Moved; - - protected void OnMoving(MoveEventArgs args) - { - Moving.RaiseEvent(args, Instance); - } - - protected bool OnMovingCancelled(MoveEventArgs args) - { - return Moving.IsRaisedEventCancelled(args, Instance); - } - - protected void OnMoved(MoveEventArgs args) - { - Moved.RaiseEvent(args, Instance); - } - - public static event TypedEventHandler> SavingContainer; - public static event TypedEventHandler> SavedContainer; - - protected void OnSavingContainer(SaveEventArgs args) - { - SavingContainer.RaiseEvent(args, Instance); - } - - protected bool OnSavingContainerCancelled(SaveEventArgs args) - { - return SavingContainer.IsRaisedEventCancelled(args, Instance); - } - - protected void OnSavedContainer(SaveEventArgs args) - { - SavedContainer.RaiseEvent(args, Instance); - } - - public static event TypedEventHandler> DeletingContainer; - public static event TypedEventHandler> DeletedContainer; - - protected void OnDeletingContainer(DeleteEventArgs args) - { - DeletingContainer.RaiseEvent(args, Instance); - } - - protected bool OnDeletingContainerCancelled(DeleteEventArgs args) - { - return DeletingContainer.IsRaisedEventCancelled(args, Instance); - } - - protected void OnDeletedContainer(DeleteEventArgs args) - { - DeletedContainer.RaiseEvent(args, Instance); - } - } - - internal abstract class ContentTypeServiceBase : ContentTypeServiceBase, IContentTypeServiceBase - where TRepository : IContentTypeRepositoryBase - where TItem : class, IContentTypeComposition - where TService : class, IContentTypeServiceBase - { - protected ContentTypeServiceBase(IDatabaseUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory) - : base(provider, logger, eventMessagesFactory) - { } - - protected abstract int[] WriteLockIds { get; } - protected abstract int[] ReadLockIds { get; } - - #region Validation - - public Attempt ValidateComposition(TItem compo) - { - try - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - uow.ReadLock(ReadLockIds); - ValidateLocked(repo, compo); - uow.Complete(); - } - return Attempt.Succeed(); - } - catch (InvalidCompositionException ex) - { - return Attempt.Fail(ex.PropertyTypeAliases, ex); - } - } - - protected void ValidateLocked(TRepository repository, TItem compositionContentType) - { - // performs business-level validation of the composition - // should ensure that it is absolutely safe to save the composition - - // eg maybe a property has been added, with an alias that's OK (no conflict with ancestors) - // but that cannot be used (conflict with descendants) - - var allContentTypes = repository.GetAll(new int[0]).Cast().ToArray(); - - var compositionAliases = compositionContentType.CompositionAliases(); - var compositions = allContentTypes.Where(x => compositionAliases.Any(y => x.Alias.Equals(y))); - var propertyTypeAliases = compositionContentType.PropertyTypes.Select(x => x.Alias.ToLowerInvariant()).ToArray(); - var indirectReferences = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == compositionContentType.Id)); - var comparer = new DelegateEqualityComparer((x, y) => x.Id == y.Id, x => x.Id); - var dependencies = new HashSet(compositions, comparer); - var stack = new Stack(); - indirectReferences.ForEach(stack.Push); // push indirect references to a stack, so we can add recursively - while (stack.Count > 0) - { - var indirectReference = stack.Pop(); - dependencies.Add(indirectReference); - // get all compositions for the current indirect reference - var directReferences = indirectReference.ContentTypeComposition; - - foreach (var directReference in directReferences) - { - if (directReference.Id == compositionContentType.Id || directReference.Alias.Equals(compositionContentType.Alias)) continue; - dependencies.Add(directReference); - // a direct reference has compositions of its own - these also need to be taken into account - var directReferenceGraph = directReference.CompositionAliases(); - allContentTypes.Where(x => directReferenceGraph.Any(y => x.Alias.Equals(y, StringComparison.InvariantCultureIgnoreCase))).ForEach(c => dependencies.Add(c)); - } - // recursive lookup of indirect references - allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == indirectReference.Id)).ForEach(stack.Push); - } - - foreach (var dependency in dependencies) - { - if (dependency.Id == compositionContentType.Id) continue; - var contentTypeDependency = allContentTypes.FirstOrDefault(x => x.Alias.Equals(dependency.Alias, StringComparison.InvariantCultureIgnoreCase)); - if (contentTypeDependency == null) continue; - var intersect = contentTypeDependency.PropertyTypes.Select(x => x.Alias.ToLowerInvariant()).Intersect(propertyTypeAliases).ToArray(); - if (intersect.Length == 0) continue; - - throw new InvalidCompositionException(compositionContentType.Alias, intersect.ToArray()); - } - } - - #endregion - - #region Composition - - internal IEnumerable> ComposeContentTypeChanges(params TItem[] contentTypes) - { - // find all content types impacted by the changes, - // - content type alias changed - // - content type property removed, or alias changed - // - content type composition removed (not testing if composition had properties...) - // - // because these are the changes that would impact the raw content data - - // note - // this is meant to run *after* uow.Commit() so must use WasPropertyDirty() everywhere - // instead of IsPropertyDirty() since dirty properties have been resetted already - - var changes = new List>(); - - foreach (var contentType in contentTypes) - { - var dirty = (IRememberBeingDirty)contentType; - - // skip new content types - var isNewContentType = dirty.WasPropertyDirty("HasIdentity"); - if (isNewContentType) - { - AddChange(changes, contentType, ContentTypeChangeTypes.RefreshOther); - continue; - } - - // alias change? - var hasAliasChanged = dirty.WasPropertyDirty("Alias"); - - // existing property alias change? - var hasAnyPropertyChangedAlias = contentType.PropertyTypes.Any(propertyType => - { - var dirtyProperty = propertyType as IRememberBeingDirty; - if (dirtyProperty == null) throw new Exception("oops"); - - // skip new properties - var isNewProperty = dirtyProperty.WasPropertyDirty("HasIdentity"); - if (isNewProperty) return false; - - // alias change? - var hasPropertyAliasBeenChanged = dirtyProperty.WasPropertyDirty("Alias"); - return hasPropertyAliasBeenChanged; - }); - - // removed properties? - var hasAnyPropertyBeenRemoved = dirty.WasPropertyDirty("HasPropertyTypeBeenRemoved"); - - // removed compositions? - var hasAnyCompositionBeenRemoved = dirty.WasPropertyDirty("HasCompositionTypeBeenRemoved"); - - // main impact on properties? - var hasPropertyMainImpact = hasAnyCompositionBeenRemoved || hasAnyPropertyBeenRemoved || hasAnyPropertyChangedAlias; - - if (hasAliasChanged || hasPropertyMainImpact) - { - // add that one, as a main change - AddChange(changes, contentType, ContentTypeChangeTypes.RefreshMain); - - if (hasPropertyMainImpact) - foreach (var c in contentType.ComposedOf(this)) - AddChange(changes, c, ContentTypeChangeTypes.RefreshMain); - } - else - { - // add that one, as an other change - AddChange(changes, contentType, ContentTypeChangeTypes.RefreshOther); - } - } - - return changes; - } - - // ensures changes contains no duplicates - private static void AddChange(ICollection> changes, TItem contentType, ContentTypeChangeTypes changeTypes) - { - var change = changes.FirstOrDefault(x => x.Item == contentType); - if (change == null) - { - changes.Add(new ContentTypeChange(contentType, changeTypes)); - return; - } - change.ChangeTypes |= changeTypes; - } - - #endregion - - #region Get, Has, Is, Count - - public TItem Get(int id) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - uow.ReadLock(ReadLockIds); - var item = repo.Get(id); - uow.Complete(); - return item; - } - } - - public TItem Get(string alias) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - uow.ReadLock(ReadLockIds); - var item = repo.Get(alias); - uow.Complete(); - return item; - } - } - - public TItem Get(Guid id) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - uow.ReadLock(ReadLockIds); - var item = repo.Get(id); - uow.Complete(); - return item; - } - } - - public IEnumerable GetAll(params int[] ids) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - uow.ReadLock(ReadLockIds); - var items = repo.GetAll(ids); - uow.Complete(); - return items; - } - } - - public IEnumerable GetAll(params Guid[] ids) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - uow.ReadLock(ReadLockIds); - // IReadRepository is explicitely implemented, need to cast the repo - var items = ((IReadRepository) repo).GetAll(ids); - uow.Complete(); - return items; - } - } - - public IEnumerable GetChildren(int id) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - uow.ReadLock(ReadLockIds); - var query = repo.QueryT.Where(x => x.ParentId == id); - var items = repo.GetByQuery(query); - uow.Complete(); - return items; - } - } - - public IEnumerable GetChildren(Guid id) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - uow.ReadLock(ReadLockIds); - var found = Get(id); - if (found == null) return Enumerable.Empty(); - var query = repo.QueryT.Where(x => x.ParentId == found.Id); - var items = repo.GetByQuery(query); - uow.Complete(); - return items; - } - } - - public bool HasChildren(int id) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - uow.ReadLock(ReadLockIds); - var query = repo.QueryT.Where(x => x.ParentId == id); - var count = repo.Count(query); - uow.Complete(); - return count > 0; - } - } - - public bool HasChildren(Guid id) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - uow.ReadLock(ReadLockIds); - var found = Get(id); - if (found == null) return false; - var query = repo.QueryT.Where(x => x.ParentId == found.Id); - var count = repo.Count(query); - uow.Complete(); - return count > 0; - } - } - - public IEnumerable GetDescendants(int id, bool andSelf) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - uow.ReadLock(ReadLockIds); - - var descendants = new List(); - if (andSelf) descendants.Add(repo.Get(id)); - var ids = new Stack(); - ids.Push(id); - - while (ids.Count > 0) - { - var i = ids.Pop(); - var query = repo.QueryT.Where(x => x.ParentId == i); - var result = repo.GetByQuery(query).ToArray(); - - foreach (var c in result) - { - descendants.Add(c); - ids.Push(c.Id); - } - } - - var descendantsA = descendants.ToArray(); - uow.Complete(); - return descendantsA; - } - } - - public IEnumerable GetComposedOf(int id) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - uow.ReadLock(ReadLockIds); - - // hash set handles duplicates - var composed = new HashSet(new DelegateEqualityComparer( - (x, y) => x.Id == y.Id, - x => x.Id.GetHashCode())); - - var ids = new Stack(); - ids.Push(id); - - while (ids.Count > 0) - { - var i = ids.Pop(); - var result = repo.GetTypesDirectlyComposedOf(i).ToArray(); - - foreach (var c in result) - { - composed.Add(c); - ids.Push(c.Id); - } - } - - var composedA = composed.ToArray(); - uow.Complete(); - return composedA; - } - } - - public int Count() - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - uow.ReadLock(ReadLockIds); - var count = repo.Count(repo.QueryT); - uow.Complete(); - return count; - } - } - - #endregion - - #region Save - - public void Save(TItem item, int userId = 0) - { - if (OnSavingCancelled(new SaveEventArgs(item))) - return; - - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - uow.WriteLock(WriteLockIds); - - // validate the DAG transform, within the lock - ValidateLocked(repo, item); // throws if invalid - - item.CreatorId = userId; - repo.AddOrUpdate(item); // also updates content/media/member items - uow.Flush(); // to db but no commit yet - - // figure out impacted content types - var changes = ComposeContentTypeChanges(item).ToArray(); - var args = changes.ToEventArgs(); - OnUowRefreshedEntity(args); - uow.Complete(); - OnChanged(args); - } - - OnSaved(new SaveEventArgs(item, false)); - Audit(AuditType.Save, $"Save {typeof(TItem).Name} performed by user", userId, item.Id); - } - - public void Save(IEnumerable items, int userId = 0) - { - var itemsA = items.ToArray(); - - if (OnSavingCancelled(new SaveEventArgs(itemsA))) - return; - - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - uow.WriteLock(WriteLockIds); - - // all-or-nothing, validate them all first - foreach (var contentType in itemsA) - { - ValidateLocked(repo, contentType); // throws if invalid - } - foreach (var contentType in itemsA) - { - contentType.CreatorId = userId; - repo.AddOrUpdate(contentType); - } - - //save it all in one go - uow.Complete(); - - // figure out impacted content types - var changes = ComposeContentTypeChanges(itemsA).ToArray(); - var args = changes.ToEventArgs(); - OnUowRefreshedEntity(args); - OnChanged(args); - } - - OnSaved(new SaveEventArgs(itemsA, false)); - Audit(AuditType.Save, $"Save {typeof(TItem).Name} performed by user", userId, -1); - } - - #endregion - - #region Delete - - public void Delete(TItem item, int userId = 0) - { - if (OnDeletingCancelled(new DeleteEventArgs(item))) - return; - - TItem[] deleted; - - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - uow.WriteLock(WriteLockIds); - - // all descendants are going to be deleted - var descendantsAndSelf = item.DescendantsAndSelf(this) - .ToArray(); - deleted = descendantsAndSelf; - - // all impacted (through composition) probably lose some properties - // don't try to be too clever here, just report them all - // do this before anything is deleted - var changed = descendantsAndSelf.SelectMany(xx => xx.ComposedOf(this)) - .Distinct() - .Except(descendantsAndSelf) - .ToArray(); - - // delete content - DeleteItemsOfTypes(descendantsAndSelf.Select(x => x.Id)); - - // finally delete the content type - // - recursively deletes all descendants - // - deletes all associated property data - // (contents of any descendant type have been deleted but - // contents of any composed (impacted) type remain but - // need to have their property data cleared) - repo.Delete(item); - uow.Flush(); // to db but no commit yet - - //... - var changes = descendantsAndSelf.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.Remove)) - .Concat(changed.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther))); - var args = changes.ToEventArgs(); - - OnUowRefreshedEntity(args); - uow.Complete(); - OnChanged(args); - } - - OnDeleted(new DeleteEventArgs(deleted, false)); - Audit(AuditType.Delete, $"Delete {typeof(TItem).Name} performed by user", userId, item.Id); - } - - public void Delete(IEnumerable items, int userId = 0) - { - var itemsA = items.ToArray(); - - if (OnDeletingCancelled(new DeleteEventArgs(itemsA))) - return; - - TItem[] deleted; - - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - uow.WriteLock(WriteLockIds); - - // all descendants are going to be deleted - var allDescendantsAndSelf = itemsA.SelectMany(xx => xx.DescendantsAndSelf(this)) - .DistinctBy(x => x.Id) - .ToArray(); - deleted = allDescendantsAndSelf; - - // all impacted (through composition) probably lose some properties - // don't try to be too clever here, just report them all - // do this before anything is deleted - var changed = allDescendantsAndSelf.SelectMany(x => x.ComposedOf(this)) - .Distinct() - .Except(allDescendantsAndSelf) - .ToArray(); - - // delete content - DeleteItemsOfTypes(allDescendantsAndSelf.Select(x => x.Id)); - - // finally delete the content types - // (see notes in overload) - foreach (var item in itemsA) - repo.Delete(item); - - uow.Flush(); // to db but no commit yet - - var changes = allDescendantsAndSelf.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.Remove)) - .Concat(changed.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther))); - var args = changes.ToEventArgs(); - - uow.Complete(); - - OnUowRefreshedEntity(args); - - OnChanged(args); - } - - OnDeleted(new DeleteEventArgs(deleted, false)); - Audit(AuditType.Delete, $"Delete {typeof(TItem).Name} performed by user", userId, -1); - } - - protected abstract void DeleteItemsOfTypes(IEnumerable typeIds); - - #endregion - - #region Copy - - public TItem Copy(TItem original, string alias, string name, int parentId = -1) - { - TItem parent = null; - if (parentId > 0) - { - parent = Get(parentId); - if (parent == null) - { - throw new InvalidOperationException("Could not find parent with id " + parentId); - } - } - return Copy(original, alias, name, parent); - } - - public TItem Copy(TItem original, string alias, string name, TItem parent) - { - Mandate.ParameterNotNull(original, "original"); - Mandate.ParameterNotNullOrEmpty(alias, "alias"); - - if (parent != null) - Mandate.That(parent.HasIdentity, () => new InvalidOperationException("The parent must have an identity")); - - // this is illegal - //var originalb = (ContentTypeCompositionBase)original; - // but we *know* it has to be a ContentTypeCompositionBase anyways - var originalb = (ContentTypeCompositionBase) (object) original; - var clone = (TItem) originalb.DeepCloneWithResetIdentities(alias); - - clone.Name = name; - - //remove all composition that is not it's current alias - var compositionAliases = clone.CompositionAliases().Except(new[] { alias }).ToList(); - foreach (var a in compositionAliases) - { - clone.RemoveContentType(a); - } - - //if a parent is specified set it's composition and parent - if (parent != null) - { - //add a new parent composition - clone.AddContentType(parent); - clone.ParentId = parent.Id; - } - else - { - //set to root - clone.ParentId = -1; - } - - Save(clone); - return clone; - } - - public Attempt> Copy(TItem copying, int containerId) - { - var evtMsgs = EventMessagesFactory.Get(); - - TItem copy; - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - uow.WriteLock(WriteLockIds); - - var containerRepository = uow.CreateContainerRepository(ContainerObjectType); - try - { - if (containerId > 0) - { - var container = containerRepository.Get(containerId); - if (container == null) - throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); // causes rollback - } - var alias = repo.GetUniqueAlias(copying.Alias); - - // this is illegal - //var copyingb = (ContentTypeCompositionBase) copying; - // but we *know* it has to be a ContentTypeCompositionBase anyways - var copyingb = (ContentTypeCompositionBase) (object)copying; - copy = (TItem) copyingb.DeepCloneWithResetIdentities(alias); - - copy.Name = copy.Name + " (copy)"; // might not be unique - - // if it has a parent, and the parent is a content type, unplug composition - // all other compositions remain in place in the copied content type - if (copy.ParentId > 0) - { - var parent = repo.Get(copy.ParentId); - if (parent != null) - copy.RemoveContentType(parent.Alias); - } - - copy.ParentId = containerId; - repo.AddOrUpdate(copy); - uow.Complete(); - } - catch (DataOperationException ex) - { - return OperationStatus.Attempt.Fail(ex.Operation, evtMsgs); // causes rollback - } - } - - return OperationStatus.Attempt.Succeed(MoveOperationStatusType.Success, evtMsgs, copy); - } - - #endregion - - #region Move - - public Attempt> Move(TItem moving, int containerId) - { - var evtMsgs = EventMessagesFactory.Get(); - - if (OnMovingCancelled(new MoveEventArgs(evtMsgs, new MoveEventInfo(moving, moving.Path, containerId)))) - return OperationStatus.Attempt.Fail(MoveOperationStatusType.FailedCancelledByEvent, evtMsgs); - - var moveInfo = new List>(); - using (var uow = UowProvider.CreateUnitOfWork()) - { - uow.WriteLock(WriteLockIds); // also for containers - - var repo = uow.CreateRepository(); - var containerRepo = uow.CreateRepository(); - - try - { - EntityContainer container = null; - if (containerId > 0) - { - container = containerRepo.Get(containerId); - if (container == null) - throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); // causes rollback - } - moveInfo.AddRange(repo.Move(moving, container)); - uow.Complete(); - } - catch (DataOperationException ex) - { - return OperationStatus.Attempt.Fail(ex.Operation, evtMsgs); // causes rollback - } - } - - // note: not raising any Changed event here because moving a content type under another container - // has no impact on the published content types - would be entirely different if we were to support - // moving a content type under another content type. - - OnMoved(new MoveEventArgs(false, evtMsgs, moveInfo.ToArray())); - - return OperationStatus.Attempt.Succeed(MoveOperationStatusType.Success, evtMsgs); - } - - #endregion - - #region Containers - - protected abstract Guid ContainedObjectType { get; } - - protected Guid ContainerObjectType => EntityContainer.GetContainerObjectType(ContainedObjectType); - - public Attempt> CreateContainer(int parentId, string name, int userId = 0) - { - var evtMsgs = EventMessagesFactory.Get(); - using (var uow = UowProvider.CreateUnitOfWork()) - { - uow.WriteLock(WriteLockIds); // also for containers - - var repo = uow.CreateContainerRepository(ContainerObjectType); - try - { - var container = new EntityContainer(Constants.ObjectTypes.DocumentTypeGuid) - { - Name = name, - ParentId = parentId, - CreatorId = userId - }; - - if (OnSavingContainerCancelled(new SaveEventArgs(container, evtMsgs))) - return OperationStatus.Attempt.Cancel(evtMsgs, container); // causes rollback - - repo.AddOrUpdate(container); - uow.Complete(); - - OnSavedContainer(new SaveEventArgs(container, evtMsgs)); - //TODO: Audit trail ? - - return OperationStatus.Attempt.Succeed(evtMsgs, container); - } - catch (Exception ex) - { - return OperationStatus.Attempt.Fail(OperationStatusType.FailedCancelledByEvent, evtMsgs, ex); - } - } - } - - public Attempt SaveContainer(EntityContainer container, int userId = 0) - { - var evtMsgs = EventMessagesFactory.Get(); - - var containerObjectType = ContainerObjectType; - if (container.ContainedObjectType != containerObjectType) - { - var ex = new InvalidOperationException("Not a container of the proper type."); - return OperationStatus.Attempt.Fail(evtMsgs, ex); - } - - if (container.HasIdentity && container.IsPropertyDirty("ParentId")) - { - var ex = new InvalidOperationException("Cannot save a container with a modified parent, move the container instead."); - return OperationStatus.Attempt.Fail(evtMsgs, ex); - } - - if (OnSavingContainerCancelled(new SaveEventArgs(container, evtMsgs))) - return OperationStatus.Attempt.Cancel(evtMsgs); - - using (var uow = UowProvider.CreateUnitOfWork()) - { - uow.WriteLock(WriteLockIds); // also for containers - - var repo = uow.CreateContainerRepository(containerObjectType); - repo.AddOrUpdate(container); - uow.Complete(); - } - - OnSavedContainer(new SaveEventArgs(container, evtMsgs)); - - //TODO: Audit trail ? - - return OperationStatus.Attempt.Succeed(evtMsgs); - } - - public EntityContainer GetContainer(int containerId) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - uow.ReadLock(ReadLockIds); // also for containers - - var repo = uow.CreateContainerRepository(ContainerObjectType); - var container = repo.Get(containerId); - uow.Complete(); - return container; - } - } - - public EntityContainer GetContainer(Guid containerId) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - uow.ReadLock(ReadLockIds); // also for containers - - var repo = uow.CreateContainerRepository(ContainerObjectType); - var container = ((EntityContainerRepository) repo).Get(containerId); - uow.Complete(); - return container; - } - } - - public IEnumerable GetContainers(int[] containerIds) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - uow.ReadLock(ReadLockIds); // also for containers - - var repo = uow.CreateContainerRepository(ContainerObjectType); - var containers = repo.GetAll(containerIds); - uow.Complete(); - return containers; - } - } - - public IEnumerable GetContainers(TItem item) - { - var ancestorIds = item.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(x => - { - var asInt = x.TryConvertTo(); - return asInt ? asInt.Result : int.MinValue; - }) - .Where(x => x != int.MinValue && x != item.Id) - .ToArray(); - - return GetContainers(ancestorIds); - } - - public IEnumerable GetContainers(string name, int level) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - uow.ReadLock(ReadLockIds); // also for containers - - var repo = uow.CreateContainerRepository(ContainerObjectType); - var containers = ((EntityContainerRepository) repo).Get(name, level); - uow.Complete(); - return containers; - } - } - - public Attempt DeleteContainer(int containerId, int userId = 0) - { - var evtMsgs = EventMessagesFactory.Get(); - using (var uow = UowProvider.CreateUnitOfWork()) - { - uow.WriteLock(WriteLockIds); // also for containers - - var repo = uow.CreateContainerRepository(ContainerObjectType); - var container = repo.Get(containerId); - if (container == null) return OperationStatus.Attempt.NoOperation(evtMsgs); - - var erepo = uow.CreateRepository(); - var entity = erepo.Get(container.Id); - if (entity.HasChildren()) // because container.HasChildren() does not work? - return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCannot, evtMsgs)); // causes rollback - - if (OnDeletingContainerCancelled(new DeleteEventArgs(container, evtMsgs))) - return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCancelledByEvent, evtMsgs)); // causes rollback - - repo.Delete(container); - uow.Complete(); - - OnDeletedContainer(new DeleteEventArgs(container, evtMsgs)); - - return OperationStatus.Attempt.Succeed(evtMsgs); - //TODO: Audit trail ? - } - } - - #endregion - - #region Audit - - private void Audit(AuditType type, string message, int userId, int objectId) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - repo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); - uow.Complete(); - } - } - - #endregion - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTItemTService.cs b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTItemTService.cs new file mode 100644 index 0000000000..73a58b1671 --- /dev/null +++ b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTItemTService.cs @@ -0,0 +1,130 @@ +using System.Runtime.CompilerServices; +using Umbraco.Core.Events; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.UnitOfWork; +using Umbraco.Core.Services.Changes; + +namespace Umbraco.Core.Services +{ + internal abstract class ContentTypeServiceBase : ContentTypeServiceBase + where TItem : class, IContentTypeComposition + where TService : class, IContentTypeServiceBase + { + protected ContentTypeServiceBase(IScopeUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory) + : base(provider, logger, eventMessagesFactory) + { } + + protected abstract TService This { get; } + + // that one must be dispatched + internal static event TypedEventHandler.EventArgs> Changed; + + // that one is always immediate (transactional) + public static event TypedEventHandler.EventArgs> UowRefreshedEntity; + + // these must be dispatched + public static event TypedEventHandler> Saving; + public static event TypedEventHandler> Saved; + public static event TypedEventHandler> Deleting; + public static event TypedEventHandler> Deleted; + public static event TypedEventHandler> Moving; + public static event TypedEventHandler> Moved; + public static event TypedEventHandler> SavingContainer; + public static event TypedEventHandler> SavedContainer; + public static event TypedEventHandler> DeletingContainer; + public static event TypedEventHandler> DeletedContainer; + + // fixme - can we have issues with event names? + + protected void OnChanged(IScopeUnitOfWork uow, ContentTypeChange.EventArgs args) + { + uow.Events.Dispatch(Changed, This, args); + } + + protected void OnUowRefreshedEntity(ContentTypeChange.EventArgs args) + { + UowRefreshedEntity.RaiseEvent(args, This); + } + + // fixme what is thsi? + protected void OnSaving(IScopeUnitOfWork uow, SaveEventArgs args) + { + Saving.RaiseEvent(args, This); + } + + protected bool OnSavingCancelled(IScopeUnitOfWork uow, SaveEventArgs args) + { + return uow.Events.DispatchCancelable(Saving, This, args); + } + + protected void OnSaved(IScopeUnitOfWork uow, SaveEventArgs args) + { + uow.Events.Dispatch(Saved, This, args); + } + + // fixme what is thsi? + protected void OnDeleting(IScopeUnitOfWork uow, DeleteEventArgs args) + { + Deleting.RaiseEvent(args, This); + } + + protected bool OnDeletingCancelled(IScopeUnitOfWork uow, DeleteEventArgs args) + { + return uow.Events.DispatchCancelable(Deleting, This, args); + } + + protected void OnDeleted(IScopeUnitOfWork uow, DeleteEventArgs args) + { + uow.Events.Dispatch(Deleted, This, args); + } + + // fixme what is thsi? + protected void OnMoving(IScopeUnitOfWork uow, MoveEventArgs args) + { + Moving.RaiseEvent(args, This); + } + + protected bool OnMovingCancelled(IScopeUnitOfWork uow, MoveEventArgs args) + { + return uow.Events.DispatchCancelable(Moving, This, args); + } + + protected void OnMoved(IScopeUnitOfWork uow, MoveEventArgs args) + { + uow.Events.Dispatch(Moved, This, args); + } + + // fixme what is this? + protected void OnSavingContainer(IScopeUnitOfWork uow, SaveEventArgs args) + { + SavingContainer.RaiseEvent(args, This); + } + + protected bool OnSavingContainerCancelled(IScopeUnitOfWork uow, SaveEventArgs args) + { + return uow.Events.DispatchCancelable(SavingContainer, This, args); + } + + protected void OnSavedContainer(IScopeUnitOfWork uow, SaveEventArgs args) + { + uow.Events.DispatchCancelable(SavedContainer, This, args); + } + + // fixme what is this? + protected void OnDeletingContainer(IScopeUnitOfWork uow, DeleteEventArgs args) + { + DeletingContainer.RaiseEvent(args, This); + } + + protected bool OnDeletingContainerCancelled(IScopeUnitOfWork uow, DeleteEventArgs args) + { + return uow.Events.DispatchCancelable(DeletingContainer, This, args); + } + + protected void OnDeletedContainer(IScopeUnitOfWork uow, DeleteEventArgs args) + { + uow.Events.Dispatch(DeletedContainer, This, args); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs new file mode 100644 index 0000000000..0873670f87 --- /dev/null +++ b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs @@ -0,0 +1,923 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Events; +using Umbraco.Core.Exceptions; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.Persistence.UnitOfWork; +using Umbraco.Core.Services.Changes; + +namespace Umbraco.Core.Services +{ + internal abstract class ContentTypeServiceBase : ContentTypeServiceBase, IContentTypeServiceBase + where TRepository : IContentTypeRepositoryBase + where TItem : class, IContentTypeComposition + where TService : class, IContentTypeServiceBase + { + protected ContentTypeServiceBase(IScopeUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory) + : base(provider, logger, eventMessagesFactory) + { } + + protected abstract int[] WriteLockIds { get; } + protected abstract int[] ReadLockIds { get; } + + #region Validation + + public Attempt ValidateComposition(TItem compo) + { + try + { + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repo = uow.CreateRepository(); + uow.ReadLock(ReadLockIds); + ValidateLocked(repo, compo); + } + return Attempt.Succeed(); + } + catch (InvalidCompositionException ex) + { + return Attempt.Fail(ex.PropertyTypeAliases, ex); + } + } + + protected void ValidateLocked(TRepository repository, TItem compositionContentType) + { + // performs business-level validation of the composition + // should ensure that it is absolutely safe to save the composition + + // eg maybe a property has been added, with an alias that's OK (no conflict with ancestors) + // but that cannot be used (conflict with descendants) + + var allContentTypes = repository.GetAll(new int[0]).Cast().ToArray(); + + var compositionAliases = compositionContentType.CompositionAliases(); + var compositions = allContentTypes.Where(x => compositionAliases.Any(y => x.Alias.Equals(y))); + var propertyTypeAliases = compositionContentType.PropertyTypes.Select(x => x.Alias.ToLowerInvariant()).ToArray(); + var indirectReferences = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == compositionContentType.Id)); + var comparer = new DelegateEqualityComparer((x, y) => x.Id == y.Id, x => x.Id); + var dependencies = new HashSet(compositions, comparer); + var stack = new Stack(); + foreach (var indirectReference in indirectReferences) + stack.Push(indirectReference); // push indirect references to a stack, so we can add recursively + while (stack.Count > 0) + { + var indirectReference = stack.Pop(); + dependencies.Add(indirectReference); + + // get all compositions for the current indirect reference + var directReferences = indirectReference.ContentTypeComposition; + foreach (var directReference in directReferences) + { + if (directReference.Id == compositionContentType.Id || directReference.Alias.Equals(compositionContentType.Alias)) continue; + dependencies.Add(directReference); + // a direct reference has compositions of its own - these also need to be taken into account + var directReferenceGraph = directReference.CompositionAliases(); + foreach (var c in allContentTypes.Where(x => directReferenceGraph.Any(y => x.Alias.Equals(y, StringComparison.InvariantCultureIgnoreCase)))) + dependencies.Add(c); + } + + // recursive lookup of indirect references + foreach (var c in allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == indirectReference.Id))) + stack.Push(c); + } + + foreach (var dependency in dependencies) + { + if (dependency.Id == compositionContentType.Id) continue; + var contentTypeDependency = allContentTypes.FirstOrDefault(x => x.Alias.Equals(dependency.Alias, StringComparison.InvariantCultureIgnoreCase)); + if (contentTypeDependency == null) continue; + var intersect = contentTypeDependency.PropertyTypes.Select(x => x.Alias.ToLowerInvariant()).Intersect(propertyTypeAliases).ToArray(); + if (intersect.Length == 0) continue; + + throw new InvalidCompositionException(compositionContentType.Alias, intersect.ToArray()); + } + } + + #endregion + + #region Composition + + internal IEnumerable> ComposeContentTypeChanges(params TItem[] contentTypes) + { + // find all content types impacted by the changes, + // - content type alias changed + // - content type property removed, or alias changed + // - content type composition removed (not testing if composition had properties...) + // + // because these are the changes that would impact the raw content data + + // note + // this is meant to run *after* uow.Commit() so must use WasPropertyDirty() everywhere + // instead of IsPropertyDirty() since dirty properties have been resetted already + + var changes = new List>(); + + foreach (var contentType in contentTypes) + { + var dirty = (IRememberBeingDirty)contentType; + + // skip new content types + var isNewContentType = dirty.WasPropertyDirty("HasIdentity"); + if (isNewContentType) + { + AddChange(changes, contentType, ContentTypeChangeTypes.RefreshOther); + continue; + } + + // alias change? + var hasAliasChanged = dirty.WasPropertyDirty("Alias"); + + // existing property alias change? + var hasAnyPropertyChangedAlias = contentType.PropertyTypes.Any(propertyType => + { + var dirtyProperty = propertyType as IRememberBeingDirty; + if (dirtyProperty == null) throw new Exception("oops"); + + // skip new properties + var isNewProperty = dirtyProperty.WasPropertyDirty("HasIdentity"); + if (isNewProperty) return false; + + // alias change? + var hasPropertyAliasBeenChanged = dirtyProperty.WasPropertyDirty("Alias"); + return hasPropertyAliasBeenChanged; + }); + + // removed properties? + var hasAnyPropertyBeenRemoved = dirty.WasPropertyDirty("HasPropertyTypeBeenRemoved"); + + // removed compositions? + var hasAnyCompositionBeenRemoved = dirty.WasPropertyDirty("HasCompositionTypeBeenRemoved"); + + // main impact on properties? + var hasPropertyMainImpact = hasAnyCompositionBeenRemoved || hasAnyPropertyBeenRemoved || hasAnyPropertyChangedAlias; + + if (hasAliasChanged || hasPropertyMainImpact) + { + // add that one, as a main change + AddChange(changes, contentType, ContentTypeChangeTypes.RefreshMain); + + if (hasPropertyMainImpact) + foreach (var c in GetComposedOf(contentType.Id)) + AddChange(changes, c, ContentTypeChangeTypes.RefreshMain); + } + else + { + // add that one, as an other change + AddChange(changes, contentType, ContentTypeChangeTypes.RefreshOther); + } + } + + return changes; + } + + // ensures changes contains no duplicates + private static void AddChange(ICollection> changes, TItem contentType, ContentTypeChangeTypes changeTypes) + { + var change = changes.FirstOrDefault(x => x.Item == contentType); + if (change == null) + { + changes.Add(new ContentTypeChange(contentType, changeTypes)); + return; + } + change.ChangeTypes |= changeTypes; + } + + #endregion + + #region Get, Has, Is, Count + + public TItem Get(int id) + { + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repo = uow.CreateRepository(); + uow.ReadLock(ReadLockIds); + return repo.Get(id); + } + } + + public TItem Get(string alias) + { + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repo = uow.CreateRepository(); + uow.ReadLock(ReadLockIds); + return repo.Get(alias); + } + } + + public TItem Get(Guid id) + { + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repo = uow.CreateRepository(); + uow.ReadLock(ReadLockIds); + return repo.Get(id); + } + } + + public IEnumerable GetAll(params int[] ids) + { + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repo = uow.CreateRepository(); + uow.ReadLock(ReadLockIds); + return repo.GetAll(ids); + } + } + + public IEnumerable GetAll(params Guid[] ids) + { + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repo = uow.CreateRepository(); + uow.ReadLock(ReadLockIds); + // IReadRepository is explicitely implemented, need to cast the repo + return ((IReadRepository) repo).GetAll(ids); + } + } + + public IEnumerable GetChildren(int id) + { + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repo = uow.CreateRepository(); + uow.ReadLock(ReadLockIds); + var query = repo.QueryT.Where(x => x.ParentId == id); + return repo.GetByQuery(query); + } + } + + public IEnumerable GetChildren(Guid id) + { + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repo = uow.CreateRepository(); + uow.ReadLock(ReadLockIds); + var found = Get(id); + if (found == null) return Enumerable.Empty(); + var query = repo.QueryT.Where(x => x.ParentId == found.Id); + return repo.GetByQuery(query); + } + } + + public bool HasChildren(int id) + { + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repo = uow.CreateRepository(); + uow.ReadLock(ReadLockIds); + var query = repo.QueryT.Where(x => x.ParentId == id); + var count = repo.Count(query); + return count > 0; + } + } + + public bool HasChildren(Guid id) + { + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repo = uow.CreateRepository(); + uow.ReadLock(ReadLockIds); + var found = Get(id); + if (found == null) return false; + var query = repo.QueryT.Where(x => x.ParentId == found.Id); + var count = repo.Count(query); + return count > 0; + } + } + + /// + /// Given the path of a content item, this will return true if the content item exists underneath a list view content item + /// + /// + /// + public bool HasContainerInPath(string contentPath) + { + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + // can use same repo for both content and media + var repo = uow.CreateRepository(); + return repo.HasContainerInPath(contentPath); + } + } + + public IEnumerable GetDescendants(int id, bool andSelf) + { + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repo = uow.CreateRepository(); + uow.ReadLock(ReadLockIds); + + var descendants = new List(); + if (andSelf) descendants.Add(repo.Get(id)); + var ids = new Stack(); + ids.Push(id); + + while (ids.Count > 0) + { + var i = ids.Pop(); + var query = repo.QueryT.Where(x => x.ParentId == i); + var result = repo.GetByQuery(query).ToArray(); + + foreach (var c in result) + { + descendants.Add(c); + ids.Push(c.Id); + } + } + + return descendants.ToArray(); + } + } + + public IEnumerable GetComposedOf(int id) + { + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repo = uow.CreateRepository(); + uow.ReadLock(ReadLockIds); + + // hash set handles duplicates + var composed = new HashSet(new DelegateEqualityComparer( + (x, y) => x.Id == y.Id, + x => x.Id.GetHashCode())); + + var ids = new Stack(); + ids.Push(id); + + while (ids.Count > 0) + { + var i = ids.Pop(); + var result = repo.GetTypesDirectlyComposedOf(i).ToArray(); + + foreach (var c in result) + { + composed.Add(c); + ids.Push(c.Id); + } + } + + return composed.ToArray(); + } + } + + public int Count() + { + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repo = uow.CreateRepository(); + uow.ReadLock(ReadLockIds); + return repo.Count(repo.QueryT); + } + } + + #endregion + + #region Save + + public void Save(TItem item, int userId = 0) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + if (OnSavingCancelled(uow, new SaveEventArgs(item))) + { + uow.Complete(); + return; + } + + var repo = uow.CreateRepository(); + uow.WriteLock(WriteLockIds); + + // validate the DAG transform, within the lock + ValidateLocked(repo, item); // throws if invalid + + item.CreatorId = userId; + if (item.Description == string.Empty) item.Description = null; + repo.AddOrUpdate(item); // also updates content/media/member items + + // figure out impacted content types + var changes = ComposeContentTypeChanges(item).ToArray(); + var args = changes.ToEventArgs(); + + uow.Flush(); // to db but no commit yet - for uow event + OnUowRefreshedEntity(args); + + OnChanged(uow, args); + OnSaved(uow, new SaveEventArgs(item, false)); + + Audit(uow, AuditType.Save, $"Save {typeof(TItem).Name} performed by user", userId, item.Id); + uow.Complete(); + } + } + + public void Save(IEnumerable items, int userId = 0) + { + var itemsA = items.ToArray(); + + using (var uow = UowProvider.CreateUnitOfWork()) + { + if (OnSavingCancelled(uow, new SaveEventArgs(itemsA))) + { + uow.Complete(); + return; + } + + var repo = uow.CreateRepository(); + uow.WriteLock(WriteLockIds); + + // all-or-nothing, validate them all first + foreach (var contentType in itemsA) + { + ValidateLocked(repo, contentType); // throws if invalid + } + foreach (var contentType in itemsA) + { + contentType.CreatorId = userId; + if (contentType.Description == string.Empty) contentType.Description = null; + repo.AddOrUpdate(contentType); + } + + // figure out impacted content types + var changes = ComposeContentTypeChanges(itemsA).ToArray(); + var args = changes.ToEventArgs(); + + uow.Flush(); // to db but no commit yet - for uow event + OnUowRefreshedEntity(args); + + OnChanged(uow, args); + OnSaved(uow, new SaveEventArgs(itemsA, false)); + + Audit(uow, AuditType.Save, $"Save {typeof(TItem).Name} performed by user", userId, -1); + uow.Complete(); + } + } + + #endregion + + #region Delete + + public void Delete(TItem item, int userId = 0) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + if (OnDeletingCancelled(uow, new DeleteEventArgs(item))) + { + uow.Complete(); + return; + } + + var repo = uow.CreateRepository(); + uow.WriteLock(WriteLockIds); + + // all descendants are going to be deleted + var descendantsAndSelf = GetDescendants(item.Id, true) + .ToArray(); + var deleted = descendantsAndSelf; + + // all impacted (through composition) probably lose some properties + // don't try to be too clever here, just report them all + // do this before anything is deleted + var changed = descendantsAndSelf.SelectMany(xx => GetComposedOf(xx.Id)) + .Distinct() + .Except(descendantsAndSelf) + .ToArray(); + + // delete content + DeleteItemsOfTypes(descendantsAndSelf.Select(x => x.Id)); + + // finally delete the content type + // - recursively deletes all descendants + // - deletes all associated property data + // (contents of any descendant type have been deleted but + // contents of any composed (impacted) type remain but + // need to have their property data cleared) + repo.Delete(item); + + //... + var changes = descendantsAndSelf.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.Remove)) + .Concat(changed.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther))); + var args = changes.ToEventArgs(); + + uow.Flush(); // to db but no commit yet - for uow event + OnUowRefreshedEntity(args); + + OnChanged(uow, args); + OnDeleted(uow, new DeleteEventArgs(deleted, false)); + + Audit(uow, AuditType.Delete, $"Delete {typeof(TItem).Name} performed by user", userId, item.Id); + uow.Complete(); + } + } + + public void Delete(IEnumerable items, int userId = 0) + { + var itemsA = items.ToArray(); + + using (var uow = UowProvider.CreateUnitOfWork()) + { + if (OnDeletingCancelled(uow, new DeleteEventArgs(itemsA))) + { + uow.Complete(); + return; + } + + var repo = uow.CreateRepository(); + uow.WriteLock(WriteLockIds); + + // all descendants are going to be deleted + var allDescendantsAndSelf = itemsA.SelectMany(xx => GetDescendants(xx.Id, true)) + .DistinctBy(x => x.Id) + .ToArray(); + var deleted = allDescendantsAndSelf; + + // all impacted (through composition) probably lose some properties + // don't try to be too clever here, just report them all + // do this before anything is deleted + var changed = allDescendantsAndSelf.SelectMany(x => GetComposedOf(x.Id)) + .Distinct() + .Except(allDescendantsAndSelf) + .ToArray(); + + // delete content + DeleteItemsOfTypes(allDescendantsAndSelf.Select(x => x.Id)); + + // finally delete the content types + // (see notes in overload) + foreach (var item in itemsA) + repo.Delete(item); + + var changes = allDescendantsAndSelf.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.Remove)) + .Concat(changed.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther))); + var args = changes.ToEventArgs(); + + uow.Flush(); // to db but no commit yet - for uow event + OnUowRefreshedEntity(args); + + OnChanged(uow, args); + OnDeleted(uow, new DeleteEventArgs(deleted, false)); + + Audit(uow, AuditType.Delete, $"Delete {typeof(TItem).Name} performed by user", userId, -1); + uow.Complete(); + } + } + + protected abstract void DeleteItemsOfTypes(IEnumerable typeIds); + + #endregion + + #region Copy + + public TItem Copy(TItem original, string alias, string name, int parentId = -1) + { + TItem parent = null; + if (parentId > 0) + { + parent = Get(parentId); + if (parent == null) + { + throw new InvalidOperationException("Could not find parent with id " + parentId); + } + } + return Copy(original, alias, name, parent); + } + + public TItem Copy(TItem original, string alias, string name, TItem parent) + { + if (original == null) throw new ArgumentNullException(nameof(original)); + if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentNullOrEmptyException(nameof(alias)); + + if (parent != null && parent.HasIdentity == false) + throw new InvalidOperationException("Parent must have an identity."); + + // this is illegal + //var originalb = (ContentTypeCompositionBase)original; + // but we *know* it has to be a ContentTypeCompositionBase anyways + var originalb = (ContentTypeCompositionBase) (object) original; + var clone = (TItem) originalb.DeepCloneWithResetIdentities(alias); + + clone.Name = name; + + //remove all composition that is not it's current alias + var compositionAliases = clone.CompositionAliases().Except(new[] { alias }).ToList(); + foreach (var a in compositionAliases) + { + clone.RemoveContentType(a); + } + + //if a parent is specified set it's composition and parent + if (parent != null) + { + //add a new parent composition + clone.AddContentType(parent); + clone.ParentId = parent.Id; + } + else + { + //set to root + clone.ParentId = -1; + } + + Save(clone); + return clone; + } + + public Attempt> Copy(TItem copying, int containerId) + { + var evtMsgs = EventMessagesFactory.Get(); + + TItem copy; + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repo = uow.CreateRepository(); + uow.WriteLock(WriteLockIds); + + var containerRepository = uow.CreateContainerRepository(ContainerObjectType); + try + { + if (containerId > 0) + { + var container = containerRepository.Get(containerId); + if (container == null) + throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); // causes rollback + } + var alias = repo.GetUniqueAlias(copying.Alias); + + // this is illegal + //var copyingb = (ContentTypeCompositionBase) copying; + // but we *know* it has to be a ContentTypeCompositionBase anyways + var copyingb = (ContentTypeCompositionBase) (object)copying; + copy = (TItem) copyingb.DeepCloneWithResetIdentities(alias); + + copy.Name = copy.Name + " (copy)"; // might not be unique + + // if it has a parent, and the parent is a content type, unplug composition + // all other compositions remain in place in the copied content type + if (copy.ParentId > 0) + { + var parent = repo.Get(copy.ParentId); + if (parent != null) + copy.RemoveContentType(parent.Alias); + } + + copy.ParentId = containerId; + repo.AddOrUpdate(copy); + uow.Complete(); + } + catch (DataOperationException ex) + { + return OperationStatus.Attempt.Fail(ex.Operation, evtMsgs); // causes rollback + } + } + + return OperationStatus.Attempt.Succeed(MoveOperationStatusType.Success, evtMsgs, copy); + } + + #endregion + + #region Move + + public Attempt> Move(TItem moving, int containerId) + { + var evtMsgs = EventMessagesFactory.Get(); + + var moveInfo = new List>(); + using (var uow = UowProvider.CreateUnitOfWork()) + { + if (OnMovingCancelled(uow, new MoveEventArgs(evtMsgs, new MoveEventInfo(moving, moving.Path, containerId)))) + { + uow.Complete(); + return OperationStatus.Attempt.Fail(MoveOperationStatusType.FailedCancelledByEvent, evtMsgs); + } + + uow.WriteLock(WriteLockIds); // also for containers + + var repo = uow.CreateRepository(); + var containerRepo = uow.CreateRepository(); + + try + { + EntityContainer container = null; + if (containerId > 0) + { + container = containerRepo.Get(containerId); + if (container == null) + throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); // causes rollback + } + moveInfo.AddRange(repo.Move(moving, container)); + uow.Complete(); + } + catch (DataOperationException ex) + { + uow.Complete(); + return OperationStatus.Attempt.Fail(ex.Operation, evtMsgs); + } + + // note: not raising any Changed event here because moving a content type under another container + // has no impact on the published content types - would be entirely different if we were to support + // moving a content type under another content type. + + OnMoved(uow, new MoveEventArgs(false, evtMsgs, moveInfo.ToArray())); + } + + return OperationStatus.Attempt.Succeed(MoveOperationStatusType.Success, evtMsgs); + } + + #endregion + + #region Containers + + protected abstract Guid ContainedObjectType { get; } + + protected Guid ContainerObjectType => EntityContainer.GetContainerObjectType(ContainedObjectType); + + public Attempt> CreateContainer(int parentId, string name, int userId = 0) + { + var evtMsgs = EventMessagesFactory.Get(); + using (var uow = UowProvider.CreateUnitOfWork()) + { + uow.WriteLock(WriteLockIds); // also for containers + + var repo = uow.CreateContainerRepository(ContainerObjectType); + try + { + var container = new EntityContainer(Constants.ObjectTypes.DocumentTypeGuid) + { + Name = name, + ParentId = parentId, + CreatorId = userId + }; + + if (OnSavingContainerCancelled(uow, new SaveEventArgs(container, evtMsgs))) + { + uow.Complete(); + return OperationStatus.Attempt.Cancel(evtMsgs, container); + } + + repo.AddOrUpdate(container); + uow.Complete(); + + OnSavedContainer(uow, new SaveEventArgs(container, evtMsgs)); + //TODO: Audit trail ? + + return OperationStatus.Attempt.Succeed(evtMsgs, container); + } + catch (Exception ex) + { + uow.Complete(); + return OperationStatus.Attempt.Fail(OperationStatusType.FailedCancelledByEvent, evtMsgs, ex); + } + } + } + + public Attempt SaveContainer(EntityContainer container, int userId = 0) + { + var evtMsgs = EventMessagesFactory.Get(); + + var containerObjectType = ContainerObjectType; + if (container.ContainedObjectType != containerObjectType) + { + var ex = new InvalidOperationException("Not a container of the proper type."); + return OperationStatus.Attempt.Fail(evtMsgs, ex); + } + + if (container.HasIdentity && container.IsPropertyDirty("ParentId")) + { + var ex = new InvalidOperationException("Cannot save a container with a modified parent, move the container instead."); + return OperationStatus.Attempt.Fail(evtMsgs, ex); + } + + using (var uow = UowProvider.CreateUnitOfWork()) + { + if (OnSavingContainerCancelled(uow, new SaveEventArgs(container, evtMsgs))) + { + uow.Complete(); + return OperationStatus.Attempt.Cancel(evtMsgs); + } + + uow.WriteLock(WriteLockIds); // also for containers + + var repo = uow.CreateContainerRepository(containerObjectType); + repo.AddOrUpdate(container); + uow.Complete(); + + OnSavedContainer(uow, new SaveEventArgs(container, evtMsgs)); + } + + //TODO: Audit trail ? + + return OperationStatus.Attempt.Succeed(evtMsgs); + } + + public EntityContainer GetContainer(int containerId) + { + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + uow.ReadLock(ReadLockIds); // also for containers + + var repo = uow.CreateContainerRepository(ContainerObjectType); + return repo.Get(containerId); + } + } + + public EntityContainer GetContainer(Guid containerId) + { + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + uow.ReadLock(ReadLockIds); // also for containers + + var repo = uow.CreateContainerRepository(ContainerObjectType); + return ((EntityContainerRepository) repo).Get(containerId); + } + } + + public IEnumerable GetContainers(int[] containerIds) + { + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + uow.ReadLock(ReadLockIds); // also for containers + + var repo = uow.CreateContainerRepository(ContainerObjectType); + return repo.GetAll(containerIds); + } + } + + public IEnumerable GetContainers(TItem item) + { + var ancestorIds = item.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => + { + var asInt = x.TryConvertTo(); + return asInt ? asInt.Result : int.MinValue; + }) + .Where(x => x != int.MinValue && x != item.Id) + .ToArray(); + + return GetContainers(ancestorIds); + } + + public IEnumerable GetContainers(string name, int level) + { + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + uow.ReadLock(ReadLockIds); // also for containers + + var repo = uow.CreateContainerRepository(ContainerObjectType); + return ((EntityContainerRepository) repo).Get(name, level); + } + } + + public Attempt DeleteContainer(int containerId, int userId = 0) + { + var evtMsgs = EventMessagesFactory.Get(); + using (var uow = UowProvider.CreateUnitOfWork()) + { + uow.WriteLock(WriteLockIds); // also for containers + + var repo = uow.CreateContainerRepository(ContainerObjectType); + var container = repo.Get(containerId); + if (container == null) return OperationStatus.Attempt.NoOperation(evtMsgs); + + var erepo = uow.CreateRepository(); + var entity = erepo.Get(container.Id); + if (entity.HasChildren()) // fixme because container.HasChildren() does not work? + { + // fixme - here and everywhere, original v8 would not Complete, thus causing rollback = ? + uow.Complete(); + return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCannot, evtMsgs)); + } + + if (OnDeletingContainerCancelled(uow, new DeleteEventArgs(container, evtMsgs))) + { + uow.Complete(); + return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCancelledByEvent, evtMsgs)); + } + + repo.Delete(container); + uow.Complete(); + + OnDeletedContainer(uow, new DeleteEventArgs(container, evtMsgs)); + + return OperationStatus.Attempt.Succeed(evtMsgs); + //TODO: Audit trail ? + } + } + + #endregion + + #region Audit + + private void Audit(IScopeUnitOfWork uow, AuditType type, string message, int userId, int objectId) + { + var repo = uow.CreateRepository(); + repo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs index 3de9e769ea..4964a20ff4 100644 --- a/src/Umbraco.Core/Services/DataTypeService.cs +++ b/src/Umbraco.Core/Services/DataTypeService.cs @@ -1,13 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading; using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Rdbms; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Exceptions; @@ -18,13 +15,11 @@ namespace Umbraco.Core.Services /// /// Represents the DataType Service, which is an easy access to operations involving /// - public class DataTypeService : RepositoryService, IDataTypeService + public class DataTypeService : ScopeRepositoryService, IDataTypeService { - - public DataTypeService(IDatabaseUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory) + public DataTypeService(IScopeUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory) : base(provider, logger, eventMessagesFactory) - { - } + { } #region Containers @@ -43,13 +38,16 @@ namespace Umbraco.Core.Services CreatorId = userId }; - if (SavingContainer.IsRaisedEventCancelled(new SaveEventArgs(container, evtMsgs), this)) - return OperationStatus.Attempt.Cancel(evtMsgs, container); // causes rollback + if (uow.Events.DispatchCancelable(SavingContainer, this, new SaveEventArgs(container, evtMsgs))) + { + uow.Complete(); + return OperationStatus.Attempt.Cancel(evtMsgs, container); + } repo.AddOrUpdate(container); uow.Complete(); - SavedContainer.RaiseEvent(new SaveEventArgs(container, evtMsgs), this); + uow.Events.Dispatch(SavedContainer, this, new SaveEventArgs(container, evtMsgs)); //TODO: Audit trail ? return OperationStatus.Attempt.Succeed(evtMsgs, container); @@ -63,34 +61,28 @@ namespace Umbraco.Core.Services public EntityContainer GetContainer(int containerId) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repo = uow.CreateRepository(); - var container = repo.Get(containerId); - uow.Complete(); - return container; + return repo.Get(containerId); } } public EntityContainer GetContainer(Guid containerId) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repo = uow.CreateRepository(); - var container = ((EntityContainerRepository)repo).Get(containerId); - uow.Complete(); - return container; + return ((EntityContainerRepository)repo).Get(containerId); } } public IEnumerable GetContainers(string name, int level) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repo = uow.CreateRepository(); - var containers = ((EntityContainerRepository)repo).Get(name, level); - uow.Complete(); - return containers; + return ((EntityContainerRepository)repo).Get(name, level); } } @@ -100,8 +92,7 @@ namespace Umbraco.Core.Services .Select(x => { var asInt = x.TryConvertTo(); - if (asInt) return asInt.Result; - return int.MinValue; + return asInt ? asInt.Result : int.MinValue; }) .Where(x => x != int.MinValue && x != dataTypeDefinition.Id) .ToArray(); @@ -111,12 +102,10 @@ namespace Umbraco.Core.Services public IEnumerable GetContainers(int[] containerIds) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repo = uow.CreateRepository(); - var containers = repo.GetAll(containerIds); - uow.Complete(); - return containers; + return repo.GetAll(containerIds); } } @@ -136,24 +125,22 @@ namespace Umbraco.Core.Services return OperationStatus.Attempt.Fail(evtMsgs, ex); } - if (SavingContainer.IsRaisedEventCancelled( - new SaveEventArgs(container, evtMsgs), - this)) - { - return OperationStatus.Attempt.Cancel(evtMsgs); - } - using (var uow = UowProvider.CreateUnitOfWork()) { + if (uow.Events.DispatchCancelable(SavingContainer, this, new SaveEventArgs(container, evtMsgs))) + { + uow.Complete(); + return OperationStatus.Attempt.Cancel(evtMsgs); + } + var repo = uow.CreateRepository(); repo.AddOrUpdate(container); + + uow.Events.Dispatch(SavedContainer, this, new SaveEventArgs(container, evtMsgs)); uow.Complete(); } - SavedContainer.RaiseEvent(new SaveEventArgs(container, evtMsgs), this); - //TODO: Audit trail ? - return OperationStatus.Attempt.Succeed(evtMsgs); } @@ -171,17 +158,20 @@ namespace Umbraco.Core.Services if (entity.HasChildren()) // because container.HasChildren() does not work? return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCannot, evtMsgs)); // causes rollback - if (DeletingContainer.IsRaisedEventCancelled(new DeleteEventArgs(container, evtMsgs), this)) - return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCancelledByEvent, evtMsgs)); // causes rollback + if (uow.Events.DispatchCancelable(DeletingContainer, this, new DeleteEventArgs(container, evtMsgs))) + { + uow.Complete(); + return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCancelledByEvent, evtMsgs)); + } repo.Delete(container); + + uow.Events.Dispatch(DeletedContainer, this, new DeleteEventArgs(container, evtMsgs)); uow.Complete(); - - DeletedContainer.RaiseEvent(new DeleteEventArgs(container, evtMsgs), this); - - return OperationStatus.Attempt.Succeed(evtMsgs); - //TODO: Audit trail ? } + + //TODO: Audit trail ? + return OperationStatus.Attempt.Succeed(evtMsgs); } #endregion @@ -193,12 +183,10 @@ namespace Umbraco.Core.Services /// public IDataTypeDefinition GetDataTypeDefinitionByName(string name) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); - var def = repository.GetByQuery(repository.QueryT.Where(x => x.Name == name)).FirstOrDefault(); - uow.Complete(); - return def; + return repository.GetByQuery(repository.QueryT.Where(x => x.Name == name)).FirstOrDefault(); } } @@ -209,12 +197,10 @@ namespace Umbraco.Core.Services /// public IDataTypeDefinition GetDataTypeDefinitionById(int id) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); - var def = repository.Get(id); - uow.Complete(); - return def; + return repository.Get(id); } } @@ -225,13 +211,11 @@ namespace Umbraco.Core.Services /// public IDataTypeDefinition GetDataTypeDefinitionById(Guid id) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); var query = repository.QueryT.Where(x => x.Key == id); - var definition = repository.GetByQuery(query).FirstOrDefault(); - uow.Complete(); - return definition; + return repository.GetByQuery(query).FirstOrDefault(); } } @@ -242,13 +226,11 @@ namespace Umbraco.Core.Services /// Collection of objects with a matching contorl id public IEnumerable GetDataTypeDefinitionByPropertyEditorAlias(string propertyEditorAlias) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); var query = repository.QueryT.Where(x => x.PropertyEditorAlias == propertyEditorAlias); - var definitions = repository.GetByQuery(query); - uow.Complete(); - return definitions; + return repository.GetByQuery(query); } } @@ -259,12 +241,10 @@ namespace Umbraco.Core.Services /// An enumerable list of objects public IEnumerable GetAllDataTypeDefinitions(params int[] ids) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); - var defs = repository.GetAll(ids); - uow.Complete(); - return defs; + return repository.GetAll(ids); } } @@ -275,16 +255,14 @@ namespace Umbraco.Core.Services /// An enumerable list of string values public IEnumerable GetPreValuesByDataTypeId(int id) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); var collection = repository.GetPreValuesCollectionByDataTypeId(id); //now convert the collection to a string list - var list = collection.FormatAsDictionary() + return collection.FormatAsDictionary() .Select(x => x.Value.Value) .ToList(); - uow.Complete(); - return list; } } @@ -295,12 +273,10 @@ namespace Umbraco.Core.Services /// public PreValueCollection GetPreValuesCollectionByDataTypeId(int id) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); - var vals = repository.GetPreValuesCollectionByDataTypeId(id); - uow.Complete(); - return vals; + return repository.GetPreValuesCollectionByDataTypeId(id); } } @@ -311,29 +287,26 @@ namespace Umbraco.Core.Services /// PreValue as a string public string GetPreValueAsString(int id) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); - var val = repository.GetPreValueAsString(id); - uow.Complete(); - return val; + return repository.GetPreValueAsString(id); } } public Attempt> Move(IDataTypeDefinition toMove, int parentId) { var evtMsgs = EventMessagesFactory.Get(); - - if (Moving.IsRaisedEventCancelled( - new MoveEventArgs(evtMsgs, new MoveEventInfo(toMove, toMove.Path, parentId)), - this)) - { - return OperationStatus.Attempt.Fail(MoveOperationStatusType.FailedCancelledByEvent, evtMsgs); - } - var moveInfo = new List>(); + using (var uow = UowProvider.CreateUnitOfWork()) { + if (uow.Events.DispatchCancelable(Moving, this, new MoveEventArgs(evtMsgs, new MoveEventInfo(toMove, toMove.Path, parentId)))) + { + uow.Complete(); + return OperationStatus.Attempt.Fail(MoveOperationStatusType.FailedCancelledByEvent, evtMsgs); + } + var containerRepository = uow.CreateRepository(); var repository = uow.CreateRepository(); @@ -347,16 +320,17 @@ namespace Umbraco.Core.Services throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); // causes rollback } moveInfo.AddRange(repository.Move(toMove, container)); + + uow.Events.Dispatch(Moved, this, new MoveEventArgs(false, evtMsgs, moveInfo.ToArray())); uow.Complete(); } catch (DataOperationException ex) { + uow.Complete(); // fixme what are we doing here exactly? return OperationStatus.Attempt.Fail(ex.Operation, evtMsgs); } } - Moved.RaiseEvent(new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), this); - return OperationStatus.Attempt.Succeed(MoveOperationStatusType.Success, evtMsgs); } @@ -367,20 +341,23 @@ namespace Umbraco.Core.Services /// Id of the user issueing the save public void Save(IDataTypeDefinition dataTypeDefinition, int userId = 0) { - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(dataTypeDefinition), this)) - return; - dataTypeDefinition.CreatorId = userId; using (var uow = UowProvider.CreateUnitOfWork()) { + if (uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(dataTypeDefinition))) + { + uow.Complete(); + return; + } + var repository = uow.CreateRepository(); repository.AddOrUpdate(dataTypeDefinition); + + uow.Events.Dispatch(Saved, this, new SaveEventArgs(dataTypeDefinition, false)); + Audit(uow, AuditType.Save, "Save DataTypeDefinition performed by user", userId, dataTypeDefinition.Id); uow.Complete(); } - - Saved.RaiseEvent(new SaveEventArgs(dataTypeDefinition, false), this); - Audit(AuditType.Save, string.Format("Save DataTypeDefinition performed by user"), userId, dataTypeDefinition.Id); } /// @@ -401,27 +378,29 @@ namespace Umbraco.Core.Services /// Boolean indicating whether or not to raise events public void Save(IEnumerable dataTypeDefinitions, int userId, bool raiseEvents) { - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(dataTypeDefinitions), this)) - return; - } + var dataTypeDefinitionsA = dataTypeDefinitions.ToArray(); using (var uow = UowProvider.CreateUnitOfWork()) { + if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(dataTypeDefinitionsA))) + { + uow.Complete(); + return; + } + var repository = uow.CreateRepository(); - foreach (var dataTypeDefinition in dataTypeDefinitions) + foreach (var dataTypeDefinition in dataTypeDefinitionsA) { dataTypeDefinition.CreatorId = userId; repository.AddOrUpdate(dataTypeDefinition); } - uow.Complete(); if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(dataTypeDefinitions, false), this); - } + uow.Events.Dispatch(Saved, this, new SaveEventArgs(dataTypeDefinitionsA, false)); + Audit(uow, AuditType.Save, "Save DataTypeDefinition performed by user", userId, -1); - Audit(AuditType.Save, string.Format("Save DataTypeDefinition performed by user"), userId, -1); + uow.Complete(); + } } /// @@ -439,8 +418,7 @@ namespace Umbraco.Core.Services var sortOrderObj = uow.Database.ExecuteScalar( "SELECT max(sortorder) FROM cmsDataTypePreValues WHERE datatypeNodeId = @DataTypeId", new { DataTypeId = dataTypeId }); - int sortOrder; - if (sortOrderObj == null || int.TryParse(sortOrderObj.ToString(), out sortOrder) == false) + if (sortOrderObj == null || int.TryParse(sortOrderObj.ToString(), out int sortOrder) == false) sortOrder = 1; foreach (var value in values) @@ -465,11 +443,10 @@ namespace Umbraco.Core.Services /// public void SavePreValues(int dataTypeId, IDictionary values) { - var dtd = this.GetDataTypeDefinitionById(dataTypeId); + var dtd = GetDataTypeDefinitionById(dataTypeId); if (dtd == null) - { throw new InvalidOperationException("No data type found for id " + dataTypeId); - } + SavePreValues(dtd, values); } @@ -502,25 +479,29 @@ namespace Umbraco.Core.Services /// public void SaveDataTypeAndPreValues(IDataTypeDefinition dataTypeDefinition, IDictionary values, int userId = 0) { - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(dataTypeDefinition), this)) - return; - - // if preValues contain the data type, override the data type definition accordingly - if (values != null && values.ContainsKey(Constants.PropertyEditors.PreValueKeys.DataValueType)) - dataTypeDefinition.DatabaseType = PropertyValueEditor.GetDatabaseType(values[Constants.PropertyEditors.PreValueKeys.DataValueType].Value); - - dataTypeDefinition.CreatorId = userId; - using (var uow = UowProvider.CreateUnitOfWork()) { + if (uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(dataTypeDefinition))) + { + uow.Complete(); + return; + } + + // if preValues contain the data type, override the data type definition accordingly + if (values != null && values.ContainsKey(Constants.PropertyEditors.PreValueKeys.DataValueType)) + dataTypeDefinition.DatabaseType = PropertyValueEditor.GetDatabaseType(values[Constants.PropertyEditors.PreValueKeys.DataValueType].Value); + + dataTypeDefinition.CreatorId = userId; + var repository = uow.CreateRepository(); repository.AddOrUpdate(dataTypeDefinition); // definition repository.AddOrUpdatePreValues(dataTypeDefinition, values); //prevalues + + uow.Events.Dispatch(Saved, this, new SaveEventArgs(dataTypeDefinition, false)); + Audit(uow, AuditType.Save, "Save DataTypeDefinition performed by user", userId, dataTypeDefinition.Id); + uow.Complete(); } - - Saved.RaiseEvent(new SaveEventArgs(dataTypeDefinition, false), this); - Audit(AuditType.Save, "Save DataTypeDefinition performed by user", userId, dataTypeDefinition.Id); } /// @@ -534,29 +515,28 @@ namespace Umbraco.Core.Services /// Optional Id of the user issueing the deletion public void Delete(IDataTypeDefinition dataTypeDefinition, int userId = 0) { - if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(dataTypeDefinition), this)) - return; - using (var uow = UowProvider.CreateUnitOfWork()) { + if (uow.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(dataTypeDefinition))) + { + uow.Complete(); + return; + } + var repository = uow.CreateRepository(); repository.Delete(dataTypeDefinition); - uow.Complete(); + + uow.Events.Dispatch(Deleted, this, new DeleteEventArgs(dataTypeDefinition, false)); + Audit(uow, AuditType.Delete, "Delete DataTypeDefinition performed by user", userId, dataTypeDefinition.Id); + + uow.Complete(); } - - Deleted.RaiseEvent(new DeleteEventArgs(dataTypeDefinition, false), this); - - Audit(AuditType.Delete, string.Format("Delete DataTypeDefinition performed by user"), userId, dataTypeDefinition.Id); } - private void Audit(AuditType type, string message, int userId, int objectId) + private void Audit(IUnitOfWork uow, AuditType type, string message, int userId, int objectId) { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - repo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); - uow.Complete(); - } + var repo = uow.CreateRepository(); + repo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); } #region Event Handlers @@ -596,7 +576,5 @@ namespace Umbraco.Core.Services /// public static event TypedEventHandler> Moved; #endregion - - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/DomainService.cs b/src/Umbraco.Core/Services/DomainService.cs index ca0184b259..840884a8cc 100644 --- a/src/Umbraco.Core/Services/DomainService.cs +++ b/src/Umbraco.Core/Services/DomainService.cs @@ -11,108 +11,100 @@ using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Services { - public class DomainService : RepositoryService, IDomainService + public class DomainService : ScopeRepositoryService, IDomainService { - public DomainService(IDatabaseUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory) + public DomainService(IScopeUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory) : base(provider, logger, eventMessagesFactory) { } public bool Exists(string domainName) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repo = uow.CreateRepository(); - var exists = repo.Exists(domainName); - uow.Complete(); - return exists; + return repo.Exists(domainName); } } public Attempt Delete(IDomain domain) { var evtMsgs = EventMessagesFactory.Get(); - if (Deleting.IsRaisedEventCancelled( - new DeleteEventArgs(domain, evtMsgs), - this)) - { - return OperationStatus.Attempt.Cancel(evtMsgs); - } using (var uow = UowProvider.CreateUnitOfWork()) { + if (uow.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(domain, evtMsgs))) + { + uow.Complete(); + return OperationStatus.Attempt.Cancel(evtMsgs); + } + var repository = uow.CreateRepository(); repository.Delete(domain); uow.Complete(); + + var args = new DeleteEventArgs(domain, false, evtMsgs); + uow.Events.Dispatch(Deleted, this, args); } - var args = new DeleteEventArgs(domain, false, evtMsgs); - Deleted.RaiseEvent(args, this); return OperationStatus.Attempt.Succeed(evtMsgs); } public IDomain GetByName(string name) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); - var domain = repository.GetByName(name); - uow.Complete(); - return domain; + return repository.GetByName(name); } } public IDomain GetById(int id) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repo = uow.CreateRepository(); - var domain = repo.Get(id); - uow.Complete(); - return domain; + return repo.Get(id); } } public IEnumerable GetAll(bool includeWildcards) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repo = uow.CreateRepository(); - var domains = repo.GetAll(includeWildcards); - uow.Complete(); - return domains; + return repo.GetAll(includeWildcards); } } public IEnumerable GetAssignedDomains(int contentId, bool includeWildcards) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repo = uow.CreateRepository(); - var domains = repo.GetAssignedDomains(contentId, includeWildcards); - uow.Complete(); - return domains; + return repo.GetAssignedDomains(contentId, includeWildcards); } } public Attempt Save(IDomain domainEntity) { var evtMsgs = EventMessagesFactory.Get(); - if (Saving.IsRaisedEventCancelled( - new SaveEventArgs(domainEntity, evtMsgs), - this)) - { - return OperationStatus.Attempt.Cancel(evtMsgs); - } using (var uow = UowProvider.CreateUnitOfWork()) { + if (uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(domainEntity, evtMsgs))) + { + uow.Complete(); + return OperationStatus.Attempt.Cancel(evtMsgs); + } + var repository = uow.CreateRepository(); repository.AddOrUpdate(domainEntity); uow.Complete(); + + uow.Events.Dispatch(Saved, this, new SaveEventArgs(domainEntity, false, evtMsgs)); } - Saved.RaiseEvent(new SaveEventArgs(domainEntity, false, evtMsgs), this); return OperationStatus.Attempt.Succeed(evtMsgs); } diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index eab2664019..d4cae25884 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -10,19 +10,20 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Services { - public class EntityService : RepositoryService, IEntityService + public class EntityService : ScopeRepositoryService, IEntityService { private readonly IRuntimeCacheProvider _runtimeCache; private readonly Dictionary>> _supportedObjectTypes; private IQuery _queryRootEntity; - public EntityService(IDatabaseUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, + public EntityService(IScopeUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, IContentService contentService, IContentTypeService contentTypeService, IMediaService mediaService, IMediaTypeService mediaTypeService, IDataTypeService dataTypeService, @@ -63,14 +64,14 @@ namespace Umbraco.Core.Services //})} }; - } #region Static Queries // lazy-constructed because when the ctor runs, the query factory may not be ready - private IQuery QueryRootEntity => _queryRootEntity ?? (_queryRootEntity = UowProvider.DatabaseFactory.Query().Where(x => x.ParentId == -1)); + private IQuery QueryRootEntity => _queryRootEntity + ?? (_queryRootEntity = UowProvider.DatabaseContext.Query().Where(x => x.ParentId == -1)); #endregion @@ -84,38 +85,16 @@ namespace Umbraco.Core.Services { var result = _runtimeCache.GetCacheItem(CacheKeys.IdToKeyCacheKey + key, () => { - int? id; - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { - switch (umbracoObjectType) - { - case UmbracoObjectTypes.Document: - case UmbracoObjectTypes.MemberType: - case UmbracoObjectTypes.Media: - case UmbracoObjectTypes.Template: - case UmbracoObjectTypes.MediaType: - case UmbracoObjectTypes.DocumentType: - case UmbracoObjectTypes.Member: - case UmbracoObjectTypes.DataType: - case UmbracoObjectTypes.DocumentTypeContainer: - id = uow.Database.ExecuteScalar( - uow.Sql() - .Select("id") - .From() - .Where(dto => dto.UniqueId == key)); - break; - case UmbracoObjectTypes.RecycleBin: - case UmbracoObjectTypes.Stylesheet: - case UmbracoObjectTypes.MemberGroup: - case UmbracoObjectTypes.ContentItem: - case UmbracoObjectTypes.ContentItemType: - case UmbracoObjectTypes.ROOT: - case UmbracoObjectTypes.Unknown: - default: - throw new NotSupportedException(); - } - uow.Complete(); - return id; + var nodeObjectType = GetNodeObjectTypeGuid(umbracoObjectType); + + var sql = uow.Sql() + .Select("id") + .From() + .Where(x => x.UniqueId == key && x.NodeObjectType == nodeObjectType); + + return uow.Database.ExecuteScalar(sql); } }); return result.HasValue ? Attempt.Succeed(result.Value) : Attempt.Fail(); @@ -131,52 +110,61 @@ namespace Umbraco.Core.Services { var result = _runtimeCache.GetCacheItem(CacheKeys.KeyToIdCacheKey + id, () => { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { - Guid? guid; - switch (umbracoObjectType) - { - case UmbracoObjectTypes.Document: - case UmbracoObjectTypes.MemberType: - case UmbracoObjectTypes.Media: - case UmbracoObjectTypes.Template: - case UmbracoObjectTypes.MediaType: - case UmbracoObjectTypes.DocumentType: - case UmbracoObjectTypes.Member: - case UmbracoObjectTypes.DataType: - guid = uow.Database.ExecuteScalar( - uow.Sql() - .Select("uniqueID") - .From() - .Where(dto => dto.NodeId == id)); - break; - case UmbracoObjectTypes.RecycleBin: - case UmbracoObjectTypes.Stylesheet: - case UmbracoObjectTypes.MemberGroup: - case UmbracoObjectTypes.ContentItem: - case UmbracoObjectTypes.ContentItemType: - case UmbracoObjectTypes.ROOT: - case UmbracoObjectTypes.Unknown: - default: - throw new NotSupportedException("Unsupported object type (" + umbracoObjectType + ")."); - } - uow.Complete(); - return guid; + var nodeObjectType = GetNodeObjectTypeGuid(umbracoObjectType); + + var sql = uow.Sql() + .Select("uniqueID") + .From() + .Where(x => x.NodeId == id && x.NodeObjectType == nodeObjectType); + return uow.Database.ExecuteScalar(sql); } }); return result.HasValue ? Attempt.Succeed(result.Value) : Attempt.Fail(); } + private static Guid GetNodeObjectTypeGuid(UmbracoObjectTypes umbracoObjectType) + { + switch (umbracoObjectType) + { + case UmbracoObjectTypes.Document: + return Constants.ObjectTypes.DocumentGuid; + case UmbracoObjectTypes.MemberType: + return Constants.ObjectTypes.MemberTypeGuid; + case UmbracoObjectTypes.Media: + return Constants.ObjectTypes.MediaGuid; + case UmbracoObjectTypes.Template: + return Constants.ObjectTypes.TemplateTypeGuid; + case UmbracoObjectTypes.MediaType: + return Constants.ObjectTypes.MediaTypeGuid; + case UmbracoObjectTypes.DocumentType: + return Constants.ObjectTypes.DocumentTypeGuid; + case UmbracoObjectTypes.Member: + return Constants.ObjectTypes.MemberGuid; + case UmbracoObjectTypes.DataType: + return Constants.ObjectTypes.DataTypeGuid; + case UmbracoObjectTypes.MemberGroup: + return Constants.ObjectTypes.MemberGroupGuid; + case UmbracoObjectTypes.RecycleBin: + case UmbracoObjectTypes.Stylesheet: + case UmbracoObjectTypes.ContentItem: + case UmbracoObjectTypes.ContentItemType: + case UmbracoObjectTypes.ROOT: + case UmbracoObjectTypes.Unknown: + default: + throw new NotSupportedException("Unsupported object type (" + umbracoObjectType + ")."); + } + } + public IUmbracoEntity GetByKey(Guid key, bool loadBaseType = true) { if (loadBaseType) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); - var entity = repository.GetByKey(key); - uow.Complete(); - return entity; + return repository.GetByKey(key); } } @@ -204,12 +192,10 @@ namespace Umbraco.Core.Services { if (loadBaseType) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); - var e = repository.Get(id); - uow.Complete(); - return e; + return repository.Get(id); } } @@ -226,12 +212,10 @@ namespace Umbraco.Core.Services if (loadBaseType) { var objectTypeId = umbracoObjectType.GetGuid(); - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); - var entity = repository.GetByKey(key, objectTypeId); - uow.Complete(); - return entity; + return repository.GetByKey(key, objectTypeId); } } @@ -260,12 +244,10 @@ namespace Umbraco.Core.Services if (loadBaseType) { var objectTypeId = umbracoObjectType.GetGuid(); - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); - var e = repository.Get(id, objectTypeId); - uow.Complete(); - return e; + return repository.Get(id, objectTypeId); } } @@ -295,21 +277,16 @@ namespace Umbraco.Core.Services { if (loadBaseType) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); - var e = repository.Get(id); - uow.Complete(); - return e; + return repository.Get(id); } } var typeFullName = typeof(T).FullName; - Mandate.That(_supportedObjectTypes.ContainsKey(typeFullName), () => - { - throw new NotSupportedException - ("The passed in type is not supported"); - }); + if (_supportedObjectTypes.ContainsKey(typeFullName) == false) + throw new NotSupportedException("The passed in type is not supported"); var entity = _supportedObjectTypes[typeFullName].Item2(id); return entity; @@ -322,16 +299,14 @@ namespace Umbraco.Core.Services /// An public virtual IUmbracoEntity GetParent(int id) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); var entity = repository.Get(id); if (entity.ParentId == -1 || entity.ParentId == -20 || entity.ParentId == -21) return null; - var e = repository.Get(entity.ParentId); - uow.Complete(); - return e; + return repository.Get(entity.ParentId); } } @@ -343,7 +318,7 @@ namespace Umbraco.Core.Services /// An public virtual IUmbracoEntity GetParent(int id, UmbracoObjectTypes umbracoObjectType) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); var entity = repository.Get(id); @@ -351,9 +326,7 @@ namespace Umbraco.Core.Services return null; var objectTypeId = umbracoObjectType.GetGuid(); - var e = repository.Get(entity.ParentId, objectTypeId); - uow.Complete(); - return e; + return repository.Get(entity.ParentId, objectTypeId); } } @@ -364,13 +337,11 @@ namespace Umbraco.Core.Services /// An enumerable list of objects public virtual IEnumerable GetChildren(int parentId) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.ParentId == parentId); - var contents = repository.GetByQuery(query); - uow.Complete(); - return contents; + return repository.GetByQuery(query); } } @@ -383,13 +354,11 @@ namespace Umbraco.Core.Services public virtual IEnumerable GetChildren(int parentId, UmbracoObjectTypes umbracoObjectType) { var objectTypeId = umbracoObjectType.GetGuid(); - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.ParentId == parentId); - var contents = repository.GetByQuery(query, objectTypeId).ToList(); // run within using! - uow.Complete(); - return contents; + return repository.GetByQuery(query, objectTypeId).ToList(); // run within using! } } @@ -400,15 +369,13 @@ namespace Umbraco.Core.Services /// An enumerable list of objects public virtual IEnumerable GetDescendents(int id) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); var entity = repository.Get(id); var pathMatch = entity.Path + ","; var query = repository.Query.Where(x => x.Path.StartsWith(pathMatch) && x.Id != id); - var entities = repository.GetByQuery(query); - uow.Complete(); - return entities; + return repository.GetByQuery(query); } } @@ -421,14 +388,118 @@ namespace Umbraco.Core.Services public virtual IEnumerable GetDescendents(int id, UmbracoObjectTypes umbracoObjectType) { var objectTypeId = umbracoObjectType.GetGuid(); - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); var entity = repository.Get(id); var query = repository.Query.Where(x => x.Path.StartsWith(entity.Path) && x.Id != id); - var entities = repository.GetByQuery(query, objectTypeId); - uow.Complete(); - return entities; + return repository.GetByQuery(query, objectTypeId); + } + } + + /// + /// Returns a paged collection of children + /// + /// The parent id to return children for + /// + /// + /// + /// + /// + /// + /// + /// + public IEnumerable GetPagedChildren(int parentId, UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = "") + { + var objectTypeId = umbracoObjectType.GetGuid(); + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repository = uow.CreateRepository(); + var query = repository.Query.Where(x => x.ParentId == parentId && x.Trashed == false); + + IQuery filterQuery = null; + if (filter.IsNullOrWhiteSpace() == false) + { + filterQuery = repository.Query.Where(x => x.Name.Contains(filter)); + } + + var contents = repository.GetPagedResultsByQuery(query, objectTypeId, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, filterQuery); + return contents; + } + } + + /// + /// Returns a paged collection of descendants + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public IEnumerable GetPagedDescendants(int id, UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "") + { + var objectTypeId = umbracoObjectType.GetGuid(); + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repository = uow.CreateRepository(); + + var query = repository.Query; + //if the id is System Root, then just get all + if (id != Constants.System.Root) + query.Where(x => x.Path.SqlContains($",{id},", TextColumnType.NVarchar)); + + IQuery filterQuery = null; + if (filter.IsNullOrWhiteSpace() == false) + { + filterQuery = repository.Query.Where(x => x.Name.Contains(filter)); + } + + var contents = repository.GetPagedResultsByQuery(query, objectTypeId, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, filterQuery); + return contents; + } + } + + /// + /// Returns a paged collection of descendants from the root + /// + /// + /// + /// + /// + /// + /// + /// + /// true/false to include trashed objects + /// + public IEnumerable GetPagedDescendantsFromRoot(UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "", bool includeTrashed = true) + { + var objectTypeId = umbracoObjectType.GetGuid(); + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repository = uow.CreateRepository(); + + var query = repository.Query; + //don't include trashed if specfied + if (includeTrashed == false) + { + query.Where(x => x.Trashed == false); + } + + IQuery filterQuery = null; + if (filter.IsNullOrWhiteSpace() == false) + { + filterQuery = repository.Query.Where(x => x.Name.Contains(filter)); + } + + var contents = repository.GetPagedResultsByQuery(query, objectTypeId, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, filterQuery); + return contents; } } @@ -440,12 +511,10 @@ namespace Umbraco.Core.Services public virtual IEnumerable GetRootEntities(UmbracoObjectTypes umbracoObjectType) { var objectTypeId = umbracoObjectType.GetGuid(); - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); - var entities = repository.GetByQuery(QueryRootEntity, objectTypeId); - uow.Complete(); - return entities; + return repository.GetByQuery(QueryRootEntity, objectTypeId); } } @@ -457,13 +526,10 @@ namespace Umbraco.Core.Services public virtual IEnumerable GetAll(params int[] ids) where T : IUmbracoEntity { var typeFullName = typeof(T).FullName; - Mandate.That(_supportedObjectTypes.ContainsKey(typeFullName), () => - { - throw new NotSupportedException - ("The passed in type is not supported"); - }); - var objectType = _supportedObjectTypes[typeFullName].Item1; + if (_supportedObjectTypes.ContainsKey(typeFullName) == false) + throw new NotSupportedException("The passed in type is not supported"); + var objectType = _supportedObjectTypes[typeFullName].Item1; return GetAll(objectType, ids); } @@ -476,40 +542,32 @@ namespace Umbraco.Core.Services public virtual IEnumerable GetAll(UmbracoObjectTypes umbracoObjectType, params int[] ids) { var entityType = GetEntityType(umbracoObjectType); + var typeFullName = entityType.FullName; - Mandate.That(_supportedObjectTypes.ContainsKey(typeFullName), () => - { - throw new NotSupportedException - ("The passed in type is not supported"); - }); + if (_supportedObjectTypes.ContainsKey(typeFullName) == false) + throw new NotSupportedException("The passed in type is not supported"); var objectTypeId = umbracoObjectType.GetGuid(); - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); - var entities = repository.GetAll(objectTypeId, ids); - uow.Complete(); - return entities; + return repository.GetAll(objectTypeId, ids); } } public IEnumerable GetAll(UmbracoObjectTypes umbracoObjectType, Guid[] keys) { var entityType = GetEntityType(umbracoObjectType); + var typeFullName = entityType.FullName; - Mandate.That(_supportedObjectTypes.ContainsKey(typeFullName), () => - { - throw new NotSupportedException - ("The passed in type is not supported"); - }); + if (_supportedObjectTypes.ContainsKey(typeFullName) == false) + throw new NotSupportedException("The passed in type is not supported"); var objectTypeId = umbracoObjectType.GetGuid(); - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); - var entities = repository.GetAll(objectTypeId, keys); - uow.Complete(); - return entities; + return repository.GetAll(objectTypeId, keys); } } @@ -523,19 +581,15 @@ namespace Umbraco.Core.Services { var umbracoObjectType = UmbracoObjectTypesExtensions.GetUmbracoObjectType(objectTypeId); var entityType = GetEntityType(umbracoObjectType); - var typeFullName = entityType.FullName; - Mandate.That(_supportedObjectTypes.ContainsKey(typeFullName), () => - { - throw new NotSupportedException - ("The passed in type is not supported"); - }); - using (var uow = UowProvider.CreateUnitOfWork()) + var typeFullName = entityType.FullName; + if (_supportedObjectTypes.ContainsKey(typeFullName) == false) + throw new NotSupportedException("The passed in type is not supported"); + + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); - var entities = repository.GetAll(objectTypeId, ids); - uow.Complete(); - return entities; + return repository.GetAll(objectTypeId, ids); } } @@ -546,7 +600,7 @@ namespace Umbraco.Core.Services /// public virtual UmbracoObjectTypes GetObjectType(int id) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var sql = uow.Sql() .Select("nodeObjectType") @@ -554,9 +608,7 @@ namespace Umbraco.Core.Services .Where(x => x.NodeId == id); var nodeObjectTypeId = uow.Database.ExecuteScalar(sql); var objectTypeId = nodeObjectTypeId; - var t = UmbracoObjectTypesExtensions.GetUmbracoObjectType(objectTypeId); - uow.Complete(); - return t; + return UmbracoObjectTypesExtensions.GetUmbracoObjectType(objectTypeId); } } @@ -567,7 +619,7 @@ namespace Umbraco.Core.Services /// public virtual UmbracoObjectTypes GetObjectType(Guid key) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var sql = uow.Sql() .Select("nodeObjectType") @@ -575,9 +627,7 @@ namespace Umbraco.Core.Services .Where(x => x.UniqueId == key); var nodeObjectTypeId = uow.Database.ExecuteScalar(sql); var objectTypeId = nodeObjectTypeId; - var t = UmbracoObjectTypesExtensions.GetUmbracoObjectType(objectTypeId); - uow.Complete(); - return t; + return UmbracoObjectTypesExtensions.GetUmbracoObjectType(objectTypeId); } } @@ -589,10 +639,9 @@ namespace Umbraco.Core.Services public virtual UmbracoObjectTypes GetObjectType(IUmbracoEntity entity) { var entityImpl = entity as UmbracoEntity; - if (entityImpl == null) - return GetObjectType(entity.Id); - - return UmbracoObjectTypesExtensions.GetUmbracoObjectType(entityImpl.NodeObjectTypeId); + return entityImpl == null + ? GetObjectType(entity.Id) + : UmbracoObjectTypesExtensions.GetUmbracoObjectType(entityImpl.NodeObjectTypeId); } /// @@ -627,5 +676,25 @@ namespace Umbraco.Core.Services return attribute.ModelType; } + + public bool Exists(Guid key) + { + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repository = uow.CreateRepository(); + var exists = repository.Exists(key); + return exists; + } + } + + public bool Exists(int id) + { + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repository = uow.CreateRepository(); + var exists = repository.Exists(id); + return exists; + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ExternalLoginService.cs b/src/Umbraco.Core/Services/ExternalLoginService.cs index cbbc3d5d47..29dc030014 100644 --- a/src/Umbraco.Core/Services/ExternalLoginService.cs +++ b/src/Umbraco.Core/Services/ExternalLoginService.cs @@ -3,15 +3,14 @@ using Microsoft.AspNet.Identity; using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models.Identity; -using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Services { - public class ExternalLoginService : RepositoryService, IExternalLoginService + public class ExternalLoginService : ScopeRepositoryService, IExternalLoginService { - public ExternalLoginService(IDatabaseUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory) + public ExternalLoginService(IScopeUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory) : base(provider, logger, eventMessagesFactory) { } @@ -22,12 +21,10 @@ namespace Umbraco.Core.Services /// public IEnumerable GetAll(int userId) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repo = uow.CreateRepository(); - var ident = repo.GetByQuery(repo.QueryT.Where(x => x.UserId == userId)); - uow.Complete(); - return ident; + return repo.GetByQuery(repo.QueryT.Where(x => x.UserId == userId)); } } @@ -39,13 +36,11 @@ namespace Umbraco.Core.Services /// public IEnumerable Find(UserLoginInfo login) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repo = uow.CreateRepository(); - var idents = repo.GetByQuery(repo.QueryT + return repo.GetByQuery(repo.QueryT .Where(x => x.ProviderKey == login.ProviderKey && x.LoginProvider == login.LoginProvider)); - uow.Complete(); - return idents; } } @@ -58,7 +53,6 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { - uow.Begin(); var repo = uow.CreateRepository(); repo.SaveUserLogins(userId, logins); uow.Complete(); @@ -73,7 +67,6 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { - uow.Begin(); var repo = uow.CreateRepository(); repo.DeleteUserLogins(userId); uow.Complete(); diff --git a/src/Umbraco.Core/Services/FileService.cs b/src/Umbraco.Core/Services/FileService.cs index 09a59282d5..70ad75ffca 100644 --- a/src/Umbraco.Core/Services/FileService.cs +++ b/src/Umbraco.Core/Services/FileService.cs @@ -9,7 +9,6 @@ using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; @@ -18,23 +17,14 @@ namespace Umbraco.Core.Services /// /// Represents the File Service, which is an easy access to operations involving objects like Scripts, Stylesheets and Templates /// - public class FileService : RepositoryService, IFileService + public class FileService : ScopeRepositoryService, IFileService { - private readonly IUnitOfWorkProvider _fileUowProvider; - private const string PartialViewHeader = "@inherits Umbraco.Web.Mvc.UmbracoTemplatePage"; private const string PartialViewMacroHeader = "@inherits Umbraco.Web.Macros.PartialViewMacroPage"; - public FileService( - IUnitOfWorkProvider fileProvider, - IDatabaseUnitOfWorkProvider dataProvider, - ILogger logger, - IEventMessagesFactory eventMessagesFactory) - : base(dataProvider, logger, eventMessagesFactory) - { - _fileUowProvider = fileProvider; - } - + public FileService(IScopeUnitOfWorkProvider uowProvider, ILogger logger, IEventMessagesFactory eventMessagesFactory) + : base(uowProvider, logger, eventMessagesFactory) + { } #region Stylesheets @@ -44,12 +34,10 @@ namespace Umbraco.Core.Services /// An enumerable list of objects public IEnumerable GetStylesheets(params string[] names) { - using (var uow = _fileUowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); - var stylesheets = repository.GetAll(names); - uow.Complete(); - return stylesheets; + return repository.GetAll(names); } } @@ -60,12 +48,10 @@ namespace Umbraco.Core.Services /// A object public Stylesheet GetStylesheetByName(string name) { - using (var uow = _fileUowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); - var stylesheet = repository.Get(name); - uow.Complete(); - return stylesheet; + return repository.Get(name); } } @@ -76,18 +62,22 @@ namespace Umbraco.Core.Services /// public void SaveStylesheet(Stylesheet stylesheet, int userId = 0) { - if (SavingStylesheet.IsRaisedEventCancelled(new SaveEventArgs(stylesheet), this)) - return; - - using (var uow = _fileUowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork()) { + if (uow.Events.DispatchCancelable(SavingStylesheet, this, new SaveEventArgs(stylesheet))) + { + uow.Complete(); + return; + } + var repository = uow.CreateRepository(); repository.AddOrUpdate(stylesheet); + + uow.Events.Dispatch(SavedStylesheet, this, new SaveEventArgs(stylesheet, false)); + + Audit(uow, AuditType.Save, "Save Stylesheet performed by user", userId, -1); uow.Complete(); } - - SavedStylesheet.RaiseEvent(new SaveEventArgs(stylesheet, false), this); - Audit(AuditType.Save, "Save Stylesheet performed by user", userId, -1); } /// @@ -97,26 +87,29 @@ namespace Umbraco.Core.Services /// public void DeleteStylesheet(string path, int userId = 0) { - Stylesheet stylesheet; - using (var uow = _fileUowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork()) { var repository = uow.CreateRepository(); - stylesheet = repository.Get(path); + var stylesheet = repository.Get(path); if (stylesheet == null) { uow.Complete(); return; } - if (DeletingStylesheet.IsRaisedEventCancelled(new DeleteEventArgs(stylesheet), this)) + if (uow.Events.DispatchCancelable(DeletingStylesheet, this, new DeleteEventArgs(stylesheet))) + { + uow.Complete(); return; // causes rollback + } repository.Delete(stylesheet); + + uow.Events.Dispatch(DeletedStylesheet, this, new DeleteEventArgs(stylesheet, false)); + + Audit(uow, AuditType.Delete, "Delete Stylesheet performed by user", userId, -1); uow.Complete(); } - - DeletedStylesheet.RaiseEvent(new DeleteEventArgs(stylesheet, false), this); - Audit(AuditType.Delete, "Delete Stylesheet performed by user", userId, -1); } /// @@ -126,23 +119,19 @@ namespace Umbraco.Core.Services /// True if Stylesheet is valid, otherwise false public bool ValidateStylesheet(Stylesheet stylesheet) { - using (var uow = _fileUowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); - var valid = repository.ValidateStylesheet(stylesheet); - uow.Complete(); - return valid; + return repository.ValidateStylesheet(stylesheet); } } public Stream GetStylesheetFileContentStream(string filepath) { - using (var uow = UowProvider.CreateUnitOfWork()) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); - var stream = repository.GetFileContentStream(filepath); - uow.Complete(); - return stream; + return repository.GetFileContentStream(filepath); } } @@ -156,6 +145,15 @@ namespace Umbraco.Core.Services } } + public long GetStylesheetFileSize(string filepath) + { + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + var repository = uow.CreateRepository(); + return repository.GetFileSize(filepath); + } + } + #endregion #region Scripts @@ -166,12 +164,10 @@ namespace Umbraco.Core.Services /// An enumerable list of objects public IEnumerable