Merge branch 'netcore/dev' of https://github.com/umbraco/Umbraco-CMS into feature/netcore-model-unit-tests-settings

This commit is contained in:
Emma Garland
2020-04-22 13:53:41 +01:00
183 changed files with 3737 additions and 1489 deletions
-1
View File
@@ -37,4 +37,3 @@ Besides "Our", we all support each other also via Twitter: [Umbraco HQ](https://
## Contributing
Umbraco is contribution-focused and community-driven. If you want to contribute back to the Umbraco source code, please check out our [guide to contributing](CONTRIBUTING.md).
+1
View File
@@ -176,3 +176,4 @@ build/temp/
/src/Umbraco.Web.UI.NetCore/wwwroot/Umbraco/lib/*
/src/Umbraco.Web.UI.NetCore/wwwroot/Umbraco/views/*
/src/Umbraco.Web.UI.NetCore/wwwroot/App_Data/TEMP/*
/src/Umbraco.Web.UI.NetCore/App_Data/Logs/*
@@ -4,7 +4,7 @@ using System.Linq;
using Umbraco.Core.Composing;
namespace Umbraco.Web.Actions
{
internal class ActionCollectionBuilder : LazyCollectionBuilderBase<ActionCollectionBuilder, ActionCollection, IAction>
public class ActionCollectionBuilder : LazyCollectionBuilderBase<ActionCollectionBuilder, ActionCollection, IAction>
{
protected override ActionCollectionBuilder This => this;
@@ -0,0 +1,190 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Umbraco.Core.Composing;
namespace Umbraco.Core.Cache
{
/// <summary>
/// Implements a fast <see cref="IAppCache"/> on top of HttpContext.Items.
/// </summary>
/// <remarks>
/// <para>If no current HttpContext items can be found (no current HttpContext,
/// or no Items...) then this cache acts as a pass-through and does not cache
/// anything.</para>
/// </remarks>
public class GenericDictionaryRequestAppCache : FastDictionaryAppCacheBase, IRequestCache
{
/// <summary>
/// Initializes a new instance of the <see cref="HttpRequestAppCache"/> class with a context, for unit tests!
/// </summary>
public GenericDictionaryRequestAppCache(Func<IDictionary<object, object>> requestItems) : base()
{
ContextItems = requestItems;
}
private Func<IDictionary<object, object>> ContextItems { get; }
public bool IsAvailable => TryGetContextItems(out _);
private bool TryGetContextItems(out IDictionary<object, object> items)
{
items = ContextItems?.Invoke();
return items != null;
}
/// <inheritdoc />
public override object Get(string key, Func<object> factory)
{
//no place to cache so just return the callback result
if (!TryGetContextItems(out var items)) return factory();
key = GetCacheKey(key);
Lazy<object> result;
try
{
EnterWriteLock();
result = items[key] as Lazy<object>; // null if key not found
// cannot create value within the lock, so if result.IsValueCreated is false, just
// do nothing here - means that if creation throws, a race condition could cause
// more than one thread to reach the return statement below and throw - accepted.
if (result == null || SafeLazy.GetSafeLazyValue(result, true) == null) // get non-created as NonCreatedValue & exceptions as null
{
result = SafeLazy.GetSafeLazy(factory);
items[key] = result;
}
}
finally
{
ExitWriteLock();
}
// using GetSafeLazy and GetSafeLazyValue ensures that we don't cache
// exceptions (but try again and again) and silently eat them - however at
// some point we have to report them - so need to re-throw here
// this does not throw anymore
//return result.Value;
var value = result.Value; // will not throw (safe lazy)
if (value is SafeLazy.ExceptionHolder eh) eh.Exception.Throw(); // throw once!
return value;
}
public bool Set(string key, object value)
{
//no place to cache so just return the callback result
if (!TryGetContextItems(out var items)) return false;
key = GetCacheKey(key);
try
{
EnterWriteLock();
items[key] = SafeLazy.GetSafeLazy(() => value);
}
finally
{
ExitWriteLock();
}
return true;
}
public bool Remove(string key)
{
//no place to cache so just return the callback result
if (!TryGetContextItems(out var items)) return false;
key = GetCacheKey(key);
try
{
EnterWriteLock();
items.Remove(key);
}
finally
{
ExitWriteLock();
}
return true;
}
#region Entries
protected override IEnumerable<DictionaryEntry> GetDictionaryEntries()
{
const string prefix = CacheItemPrefix + "-";
if (!TryGetContextItems(out var items)) return Enumerable.Empty<DictionaryEntry>();
return items.Cast<DictionaryEntry>()
.Where(x => x.Key is string s && s.StartsWith(prefix));
}
protected override void RemoveEntry(string key)
{
if (!TryGetContextItems(out var items)) return;
items.Remove(key);
}
protected override object GetEntry(string key)
{
return !TryGetContextItems(out var items) ? null : items[key];
}
#endregion
#region Lock
private const string ContextItemsLockKey = "Umbraco.Core.Cache.HttpRequestCache::LockEntered";
protected override void EnterReadLock() => EnterWriteLock();
protected override void EnterWriteLock()
{
if (!TryGetContextItems(out var items)) return;
// note: cannot keep 'entered' as a class variable here,
// since there is one per request - so storing it within
// ContextItems - which is locked, so this should be safe
var entered = false;
Monitor.Enter(items, ref entered);
items[ContextItemsLockKey] = entered;
}
protected override void ExitReadLock() => ExitWriteLock();
protected override void ExitWriteLock()
{
if (!TryGetContextItems(out var items)) return;
var entered = (bool?)items[ContextItemsLockKey] ?? false;
if (entered)
Monitor.Exit(items);
items.Remove(ContextItemsLockKey);
}
#endregion
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
{
if (!TryGetContextItems(out var items))
{
yield break;
}
foreach (var item in items)
{
yield return new KeyValuePair<string, object>(item.Key.ToString(), item.Value);
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
+3 -3
View File
@@ -21,7 +21,7 @@ namespace Umbraco.Core
/// </summary>
/// <param name="composition">The composition.</param>
/// <returns></returns>
internal static ActionCollectionBuilder Actions(this Composition composition)
public static ActionCollectionBuilder Actions(this Composition composition)
=> composition.WithCollectionBuilder<ActionCollectionBuilder>();
/// <summary>
@@ -45,7 +45,7 @@ namespace Umbraco.Core
/// </summary>
/// <param name="composition">The composition.</param>
/// <returns></returns>
internal static EditorValidatorCollectionBuilder EditorValidators(this Composition composition)
public static EditorValidatorCollectionBuilder EditorValidators(this Composition composition)
=> composition.WithCollectionBuilder<EditorValidatorCollectionBuilder>();
/// <summary>
@@ -81,7 +81,7 @@ namespace Umbraco.Core
/// <param name="composition">The composition.</param>
public static SectionCollectionBuilder Sections(this Composition composition)
=> composition.WithCollectionBuilder<SectionCollectionBuilder>();
/// <summary>
/// Gets the components collection builder.
/// </summary>
@@ -31,5 +31,20 @@ namespace Umbraco.Core
/// </summary>
public static void RegisterUnique<TService>(this Composition composition, TService instance)
=> composition.RegisterUnique(typeof(TService), instance);
/// <summary>
/// Registers a unique service with an implementation type.
/// </summary>
public static void RegisterMultipleUnique<TService1, TService2, TImplementing>(this Composition composition)
where TImplementing : class, TService1, TService2
where TService1 : class
where TService2 : class
{
composition.RegisterUnique<TImplementing>();
composition.RegisterUnique<TService1>(factory => factory.GetInstance<TImplementing>());
composition.RegisterUnique<TService2>(factory => factory.GetInstance<TImplementing>());
}
}
}
}
@@ -0,0 +1,20 @@
namespace Umbraco.Core
{
public static partial class Constants
{
public static class SqlTemplates
{
public static class VersionableRepository
{
public const string GetVersionIds = "Umbraco.Core.VersionableRepository.GetVersionIds";
public const string GetVersion = "Umbraco.Core.VersionableRepository.GetVersion";
public const string GetVersions = "Umbraco.Core.VersionableRepository.GetVersions";
public const string EnsureUniqueNodeName = "Umbraco.Core.VersionableRepository.EnsureUniqueNodeName";
public const string GetSortOrder = "Umbraco.Core.VersionableRepository.GetSortOrder";
public const string GetParentNode = "Umbraco.Core.VersionableRepository.GetParentNode";
public const string GetReservedId = "Umbraco.Core.VersionableRepository.GetReservedId";
}
}
}
}
@@ -7,7 +7,7 @@ using Umbraco.Core.Models.Membership;
namespace Umbraco.Web.ContentApps
{
internal class ContentEditorContentAppFactory : IContentAppFactory
public class ContentEditorContentAppFactory : IContentAppFactory
{
// see note on ContentApp
internal const int Weight = -100;
@@ -10,7 +10,7 @@ using Umbraco.Web.Models.ContentEditing;
namespace Umbraco.Web.ContentApps
{
internal class ListViewContentAppFactory : IContentAppFactory
public class ListViewContentAppFactory : IContentAppFactory
{
// see note on ContentApp
private const int Weight = -666;
+1
View File
@@ -6,6 +6,7 @@ namespace Umbraco.Core
{
public static class ContentExtensions
{
#region XML methods
/// <summary>
@@ -3,7 +3,7 @@ using Umbraco.Core.Events;
namespace Umbraco.Web
{
internal class DefaultEventMessagesFactory : IEventMessagesFactory
public class DefaultEventMessagesFactory : IEventMessagesFactory
{
private readonly IEventMessagesAccessor _eventMessagesAccessor;
@@ -6,6 +6,10 @@ namespace Umbraco.Core.Hosting
{
string SiteName { get; }
string ApplicationId { get; }
/// <summary>
/// Will return the physical path to the root of the application
/// </summary>
string ApplicationPhysicalPath { get; }
string LocalTempPath { get; }
@@ -27,10 +31,22 @@ namespace Umbraco.Core.Hosting
bool IsHosted { get; }
Version IISVersion { get; }
// TODO: Should we change this name to MapPathWebRoot ? and also have a new MapPathContentRoot ?
/// <summary>
/// Maps a virtual path to a physical path to the application's web root
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
/// <remarks>
/// Depending on the runtime 'web root', this result can vary. For example in Net Framework the web root and the content root are the same, however
/// in netcore the web root is /www therefore this will Map to a physical path within www.
/// </remarks>
string MapPath(string path);
/// <summary>
/// Maps a virtual path to the application's web root
/// Converts a virtual path to an absolute URL path based on the application's web root
/// </summary>
/// <param name="virtualPath">The virtual path. Must start with either ~/ or / else an exception is thrown.</param>
/// <returns></returns>
@@ -3,7 +3,7 @@ using Umbraco.Core.Events;
namespace Umbraco.Web
{
internal class HybridEventMessagesAccessor : HybridAccessorBase<EventMessages>, IEventMessagesAccessor
public class HybridEventMessagesAccessor : HybridAccessorBase<EventMessages>, IEventMessagesAccessor
{
protected override string ItemKey => "Umbraco.Core.Events.HybridEventMessagesAccessor";
@@ -0,0 +1,13 @@
namespace Umbraco.Core.Logging
{
public interface ILoggingConfiguration
{
/// <summary>
/// The physical path where logs are stored
/// </summary>
string LogDirectory { get; }
string LogConfigurationFile { get; }
string UserLogConfigurationFile { get; }
}
}
+1 -7
View File
@@ -2,18 +2,12 @@
namespace Umbraco.Core.Logging
{
/// <summary>
/// Defines the profiling service.
/// </summary>
public interface IProfiler
{
/// <summary>
/// Renders the profiling results.
/// </summary>
/// <returns>The profiling results.</returns>
/// <remarks>Generally used for HTML rendering.</remarks>
string Render();
/// <summary>
/// Gets an <see cref="IDisposable"/> that will time the code between its creation and disposal.
/// </summary>
+15
View File
@@ -0,0 +1,15 @@
namespace Umbraco.Core.Logging
{
/// <summary>
/// Used to render a profiler in a web page
/// </summary>
public interface IProfilerHtml
{
/// <summary>
/// Renders the profiling results.
/// </summary>
/// <returns>The profiling results.</returns>
/// <remarks>Generally used for HTML rendering.</remarks>
string Render();
}
}
-6
View File
@@ -15,12 +15,6 @@ namespace Umbraco.Core.Logging
_logger = logger;
}
/// <inheritdoc/>
public string Render()
{
return string.Empty;
}
/// <inheritdoc/>
public IDisposable Step(string name)
{
@@ -0,0 +1,20 @@
using System;
namespace Umbraco.Core.Logging
{
public class LoggingConfiguration : ILoggingConfiguration
{
public LoggingConfiguration(string logDirectory, string logConfigurationFile, string userLogConfigurationFile)
{
LogDirectory = logDirectory ?? throw new ArgumentNullException(nameof(logDirectory));
LogConfigurationFile = logConfigurationFile ?? throw new ArgumentNullException(nameof(logConfigurationFile));
UserLogConfigurationFile = userLogConfigurationFile ?? throw new ArgumentNullException(nameof(userLogConfigurationFile));
}
public string LogDirectory { get; }
public string LogConfigurationFile { get; }
public string UserLogConfigurationFile { get; }
}
}
-5
View File
@@ -6,11 +6,6 @@ namespace Umbraco.Core.Logging
{
private readonly VoidDisposable _disposable = new VoidDisposable();
public string Render()
{
return string.Empty;
}
public IDisposable Step(string name)
{
return _disposable;
@@ -0,0 +1,48 @@
using System.Collections.Generic;
using System.Linq;
namespace Umbraco.Core.Models
{
public class ContentDataIntegrityReport
{
public ContentDataIntegrityReport(IReadOnlyDictionary<int, ContentDataIntegrityReportEntry> detectedIssues)
{
DetectedIssues = detectedIssues;
}
public bool Ok => DetectedIssues.Count == 0 || DetectedIssues.Count == DetectedIssues.Values.Count(x => x.Fixed);
public IReadOnlyDictionary<int, ContentDataIntegrityReportEntry> DetectedIssues { get; }
public IReadOnlyDictionary<int, ContentDataIntegrityReportEntry> FixedIssues
=> DetectedIssues.Where(x => x.Value.Fixed).ToDictionary(x => x.Key, x => x.Value);
public enum IssueType
{
/// <summary>
/// The item's level and path are inconsistent with it's parent's path and level
/// </summary>
InvalidPathAndLevelByParentId,
/// <summary>
/// The item's path doesn't contain all required parts
/// </summary>
InvalidPathEmpty,
/// <summary>
/// The item's path parts are inconsistent with it's level value
/// </summary>
InvalidPathLevelMismatch,
/// <summary>
/// The item's path does not end with it's own ID
/// </summary>
InvalidPathById,
/// <summary>
/// The item's path does not have it's parent Id as the 2nd last entry
/// </summary>
InvalidPathByParentId,
}
}
}
@@ -0,0 +1,13 @@
namespace Umbraco.Core.Models
{
public class ContentDataIntegrityReportEntry
{
public ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType issueType)
{
IssueType = issueType;
}
public ContentDataIntegrityReport.IssueType IssueType { get; }
public bool Fixed { get; set; }
}
}
@@ -0,0 +1,13 @@
namespace Umbraco.Core.Models
{
public class ContentDataIntegrityReportOptions
{
/// <summary>
/// Set to true to try to automatically resolve data integrity issues
/// </summary>
public bool FixIssues { get; set; }
// TODO: We could define all sorts of options for the data integrity check like what to check for, what to fix, etc...
// things like Tag data consistency, etc...
}
}
+11 -5
View File
@@ -1,5 +1,6 @@
using System;
using System.Runtime.Serialization;
using Umbraco.Core.Exceptions;
using Umbraco.Core.Models.Entities;
namespace Umbraco.Core.Models
@@ -13,20 +14,25 @@ namespace Umbraco.Core.Models
{
private string _name;
private string _alias;
private bool _isBidrectional;
private bool _isBidirectional;
private Guid? _parentObjectType;
private Guid? _childObjectType;
public RelationType(string alias, string name)
: this(name, alias, false, null, null)
: this(name: name, alias: alias, false, null, null)
{
}
public RelationType(string name, string alias, bool isBidrectional, Guid? parentObjectType, Guid? childObjectType)
{
if (name == null) throw new ArgumentNullException(nameof(name));
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name));
if (alias == null) throw new ArgumentNullException(nameof(alias));
if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(alias));
_name = name;
_alias = alias;
_isBidrectional = isBidrectional;
_isBidirectional = isBidrectional;
_parentObjectType = parentObjectType;
_childObjectType = childObjectType;
}
@@ -57,8 +63,8 @@ namespace Umbraco.Core.Models
[DataMember]
public bool IsBidirectional
{
get => _isBidrectional;
set => SetPropertyValueAndDetectChanges(value, ref _isBidrectional, nameof(IsBidirectional));
get => _isBidirectional;
set => SetPropertyValueAndDetectChanges(value, ref _isBidirectional, nameof(IsBidirectional));
}
/// <summary>
@@ -0,0 +1,7 @@
namespace Umbraco.Net
{
public class NullSessionIdResolver : ISessionIdResolver
{
public string SessionId => null;
}
}
@@ -1,9 +1,16 @@
namespace Umbraco.Core.Services
using Umbraco.Core.Models;
namespace Umbraco.Core.Services
{
/// <summary>
/// Placeholder for sharing logic between the content, media (and member) services
/// TODO: Start sharing the logic!
/// </summary>
public interface IContentServiceBase : IService
{ }
{
/// <summary>
/// Checks/fixes the data integrity of node paths/levels stored in the database
/// </summary>
ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options);
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ using Umbraco.Web.Sections;
namespace Umbraco.Web.Services
{
internal class SectionService : ISectionService
public class SectionService : ISectionService
{
private readonly IUserService _userService;
private readonly SectionCollection _sectionCollection;
+1 -1
View File
@@ -9,7 +9,7 @@ namespace Umbraco.Web.Services
/// <summary>
/// Implements <see cref="ITreeService"/>.
/// </summary>
internal class TreeService : ITreeService
public class TreeService : ITreeService
{
private readonly TreeCollection _treeCollection;
+2 -2
View File
@@ -2,7 +2,7 @@ namespace Umbraco.Web
{
public interface ISessionManager
{
object GetSessionValue(string sessionName);
void SetSessionValue(string sessionName, object value);
string GetSessionValue(string sessionName);
void SetSessionValue(string sessionName, string value);
}
}
@@ -13,8 +13,17 @@ namespace Umbraco.Core.Composing
/// <param name="builder"></param>
/// <returns></returns>
public static IHostBuilder UseUmbraco(this IHostBuilder builder)
=> builder.UseUmbraco(new UmbracoServiceProviderFactory());
{
return builder
.UseUmbraco(new UmbracoServiceProviderFactory());
}
/// <summary>
/// Assigns a custom service provider factory to use Umbraco's container
/// </summary>
/// <param name="builder"></param>
/// <param name="umbracoServiceProviderFactory"></param>
/// <returns></returns>
public static IHostBuilder UseUmbraco(this IHostBuilder builder, UmbracoServiceProviderFactory umbracoServiceProviderFactory)
=> builder.UseServiceProviderFactory(umbracoServiceProviderFactory);
}
@@ -11,6 +11,8 @@ using Umbraco.Core.Persistence.Mappers;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Strings;
using Umbraco.Core.Sync;
using Umbraco.Web.Media.EmbedProviders;
using Umbraco.Web.Search;
namespace Umbraco.Core
{
@@ -84,7 +86,20 @@ namespace Umbraco.Core
public static ManifestFilterCollectionBuilder ManifestFilters(this Composition composition)
=> composition.WithCollectionBuilder<ManifestFilterCollectionBuilder>();
/// <summary>
/// Gets the backoffice OEmbed Providers collection builder.
/// </summary>
/// <param name="composition">The composition.</param>
public static EmbedProvidersCollectionBuilder OEmbedProviders(this Composition composition)
=> composition.WithCollectionBuilder<EmbedProvidersCollectionBuilder>();
/// <summary>
/// Gets the back office searchable tree collection builder
/// </summary>
/// <param name="composition"></param>
/// <returns></returns>
public static SearchableTreeCollectionBuilder SearchableTrees(this Composition composition)
=> composition.WithCollectionBuilder<SearchableTreeCollectionBuilder>();
#endregion
@@ -98,7 +113,7 @@ namespace Umbraco.Core
public static void SetCultureDictionaryFactory<T>(this Composition composition)
where T : ICultureDictionaryFactory
{
composition.RegisterUnique<ICultureDictionaryFactory, T>();
composition.RegisterUnique<ICultureDictionaryFactory, T>();
}
/// <summary>
@@ -4,11 +4,9 @@ using System.IO;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Composing;
using Umbraco.Core.IO;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
@@ -16,6 +14,20 @@ namespace Umbraco.Core
{
public static class ContentExtensions
{
internal static bool IsMoving(this IContentBase entity)
{
// Check if this entity is being moved as a descendant as part of a bulk moving operations.
// When this occurs, only Path + Level + UpdateDate are being changed. In this case we can bypass a lot of the below
// operations which will make this whole operation go much faster. When moving we don't need to create
// new versions, etc... because we cannot roll this operation back anyways.
var isMoving = entity.IsPropertyDirty(nameof(entity.Path))
&& entity.IsPropertyDirty(nameof(entity.Level))
&& entity.IsPropertyDirty(nameof(entity.UpdateDate));
return isMoving;
}
/// <summary>
/// Removes characters that are not valid XML characters from all entity properties
/// of type string. See: http://stackoverflow.com/a/961504/5018
@@ -3,6 +3,7 @@ using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using Umbraco.Core.Composing;
using Umbraco.Core.Hosting;
using Umbraco.Core.IO;
namespace Umbraco.Core.Diagnostics
@@ -100,7 +101,7 @@ namespace Umbraco.Core.Diagnostics
return bRet;
}
public static bool Dump(IMarchal marchal, IIOHelper ioHelper, Option options = Option.WithFullMemory, bool withException = false)
public static bool Dump(IMarchal marchal, IHostingEnvironment hostingEnvironment, Option options = Option.WithFullMemory, bool withException = false)
{
lock (LockO)
{
@@ -110,7 +111,7 @@ namespace Umbraco.Core.Diagnostics
// filter everywhere in our code = not!
var stacktrace = withException ? Environment.StackTrace : string.Empty;
var filepath = ioHelper.MapPath("~/App_Data/MiniDump");
var filepath = Path.Combine(hostingEnvironment.ApplicationPhysicalPath, "App_Data/MiniDump");
if (Directory.Exists(filepath) == false)
Directory.CreateDirectory(filepath);
@@ -122,11 +123,11 @@ namespace Umbraco.Core.Diagnostics
}
}
public static bool OkToDump(IIOHelper ioHelper)
public static bool OkToDump(IHostingEnvironment hostingEnvironment)
{
lock (LockO)
{
var filepath = ioHelper.MapPath("~/App_Data/MiniDump");
var filepath = Path.Combine(hostingEnvironment.ApplicationPhysicalPath, "App_Data/MiniDump");
if (Directory.Exists(filepath) == false) return true;
var count = Directory.GetFiles(filepath, "*.dmp").Length;
return count < 8;
@@ -16,14 +16,14 @@ namespace Umbraco.Core.Logging
// but it only has a pre-release NuGet package. So, we've got to use Serilog's code, which
// means we cannot get rid of Serilog entirely. We may want to revisit this at some point.
// TODO: Do we still need this, is there a non-pre release package shipped?
private static readonly Lazy<global::Serilog.ILogger> MinimalLogger = new Lazy<global::Serilog.ILogger>(() => new LoggerConfiguration().CreateLogger());
public string Render(string messageTemplate, params object[] args)
{
// by default, unless initialized otherwise, Log.Logger is SilentLogger which cannot bind message
// templates. Log.Logger is set to a true Logger when initializing Umbraco's logger, but in case
// that has not been done already - use a temp minimal logger (eg for tests).
var logger = Log.Logger as global::Serilog.Core.Logger ?? MinimalLogger.Value;
// resolve a minimal logger instance which is used to bind message templates
var logger = MinimalLogger.Value;
var bound = logger.BindMessageTemplate(messageTemplate, args, out var parsedTemplate, out var boundProperties);
@@ -11,13 +11,13 @@ namespace Umbraco.Core.Logging.Serilog.Enrichers
/// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpRequestIdEnricher.cs
/// Nupkg: 'Serilog.Web.Classic' contains handlers & extra bits we do not want
/// </summary>
internal class HttpRequestIdEnricher : ILogEventEnricher
public class HttpRequestIdEnricher : ILogEventEnricher
{
private readonly Func<IRequestCache> _requestCacheGetter;
private readonly IRequestCache _requestCache;
public HttpRequestIdEnricher(Func<IRequestCache> requestCacheGetter)
public HttpRequestIdEnricher(IRequestCache requestCache)
{
_requestCacheGetter = requestCacheGetter;
_requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache));
}
/// <summary>
@@ -34,11 +34,8 @@ namespace Umbraco.Core.Logging.Serilog.Enrichers
{
if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));
var requestCache = _requestCacheGetter();
if(requestCache is null) return;
Guid requestId;
if (!LogHttpRequest.TryGetCurrentHttpRequestId(out requestId, requestCache))
if (!LogHttpRequest.TryGetCurrentHttpRequestId(out requestId, _requestCache))
return;
var requestIdProperty = new LogEventProperty(HttpRequestIdPropertyName, new ScalarValue(requestId));
@@ -13,9 +13,9 @@ namespace Umbraco.Core.Logging.Serilog.Enrichers
/// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpRequestNumberEnricher.cs
/// Nupkg: 'Serilog.Web.Classic' contains handlers & extra bits we do not want
/// </summary>
internal class HttpRequestNumberEnricher : ILogEventEnricher
public class HttpRequestNumberEnricher : ILogEventEnricher
{
private readonly Func<IRequestCache> _requestCacheGetter;
private readonly IRequestCache _requestCache;
private static int _lastRequestNumber;
private static readonly string _requestNumberItemName = typeof(HttpRequestNumberEnricher).Name + "+RequestNumber";
@@ -25,9 +25,9 @@ namespace Umbraco.Core.Logging.Serilog.Enrichers
private const string _httpRequestNumberPropertyName = "HttpRequestNumber";
public HttpRequestNumberEnricher(Func<IRequestCache> requestCacheGetter)
public HttpRequestNumberEnricher(IRequestCache requestCache)
{
_requestCacheGetter = requestCacheGetter;
_requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache));
}
/// <summary>
@@ -39,10 +39,7 @@ namespace Umbraco.Core.Logging.Serilog.Enrichers
{
if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));
var requestCache = _requestCacheGetter();
if (requestCache is null) return;
var requestNumber = requestCache.Get(_requestNumberItemName,
var requestNumber = _requestCache.Get(_requestNumberItemName,
() => Interlocked.Increment(ref _lastRequestNumber));
var requestNumberProperty = new LogEventProperty(_httpRequestNumberPropertyName, new ScalarValue(requestNumber));
@@ -10,7 +10,7 @@ namespace Umbraco.Core.Logging.Serilog.Enrichers
/// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpSessionIdEnricher.cs
/// Nupkg: 'Serilog.Web.Classic' contains handlers & extra bits we do not want
/// </summary>
internal class HttpSessionIdEnricher : ILogEventEnricher
public class HttpSessionIdEnricher : ILogEventEnricher
{
private readonly ISessionIdResolver _sessionIdResolver;
@@ -0,0 +1,96 @@
using System;
using System.Reflection;
using System.Threading;
using Serilog.Core;
using Serilog.Events;
using Umbraco.Core.Configuration;
using Umbraco.Core.Diagnostics;
using Umbraco.Core.Hosting;
namespace Umbraco.Infrastructure.Logging.Serilog.Enrichers
{
/// <summary>
/// Enriches the log if there are ThreadAbort exceptions and will automatically create a minidump if it can
/// </summary>
public class ThreadAbortExceptionEnricher : ILogEventEnricher
{
private readonly ICoreDebugSettings _coreDebugSettings;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IMarchal _marchal;
public ThreadAbortExceptionEnricher(ICoreDebugSettings coreDebugSettings, IHostingEnvironment hostingEnvironment, IMarchal marchal)
{
_coreDebugSettings = coreDebugSettings;
_hostingEnvironment = hostingEnvironment;
_marchal = marchal;
}
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
switch (logEvent.Level)
{
case LogEventLevel.Error:
case LogEventLevel.Fatal:
DumpThreadAborts(logEvent, propertyFactory);
break;
}
}
private void DumpThreadAborts(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
if (!IsTimeoutThreadAbortException(logEvent.Exception)) return;
var message = "The thread has been aborted, because the request has timed out.";
// dump if configured, or if stacktrace contains Monitor.ReliableEnter
var dump = _coreDebugSettings.DumpOnTimeoutThreadAbort || IsMonitorEnterThreadAbortException(logEvent.Exception);
// dump if it is ok to dump (might have a cap on number of dump...)
dump &= MiniDump.OkToDump(_hostingEnvironment);
if (!dump)
{
message += ". No minidump was created.";
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message));
}
else
try
{
var dumped = MiniDump.Dump(_marchal, _hostingEnvironment, withException: true);
message += dumped
? ". A minidump was created in App_Data/MiniDump."
: ". Failed to create a minidump.";
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message));
}
catch (Exception ex)
{
message = "Failed to create a minidump. " + ex;
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message));
}
}
private static bool IsTimeoutThreadAbortException(Exception exception)
{
if (!(exception is ThreadAbortException abort)) return false;
if (abort.ExceptionState == null) return false;
var stateType = abort.ExceptionState.GetType();
if (stateType.FullName != "System.Web.HttpApplication+CancelModuleException") return false;
var timeoutField = stateType.GetField("_timeout", BindingFlags.Instance | BindingFlags.NonPublic);
if (timeoutField == null) return false;
return (bool)timeoutField.GetValue(abort.ExceptionState);
}
private static bool IsMonitorEnterThreadAbortException(Exception exception)
{
if (!(exception is ThreadAbortException abort)) return false;
var stacktrace = abort.StackTrace;
return stacktrace.Contains("System.Threading.Monitor.ReliableEnter");
}
}
}
@@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Text;
using Serilog;
using Serilog.Configuration;
@@ -6,11 +7,8 @@ using Serilog.Core;
using Serilog.Events;
using Serilog.Formatting;
using Serilog.Formatting.Compact;
using Umbraco.Core.Cache;
using Umbraco.Core.Composing;
using Umbraco.Core.Hosting;
using Umbraco.Core.Logging.Serilog.Enrichers;
using Umbraco.Net;
namespace Umbraco.Core.Logging.Serilog
{
@@ -24,27 +22,30 @@ namespace Umbraco.Core.Logging.Serilog
/// It is highly recommended that you keep/use this default in your own logging config customizations
/// </summary>
/// <param name="logConfig">A Serilog LoggerConfiguration</param>
/// <param name="hostingEnvironment"></param>
public static LoggerConfiguration MinimalConfiguration(this LoggerConfiguration logConfig, IHostingEnvironment hostingEnvironment, ISessionIdResolver sessionIdResolver, Func<IRequestCache> requestCacheGetter)
/// <param name="IHostingEnvironment"></param>
/// <param name="loggingConfiguration"></param>
public static LoggerConfiguration MinimalConfiguration(
this LoggerConfiguration logConfig,
IHostingEnvironment hostingEnvironment,
ILoggingConfiguration loggingConfiguration)
{
global::Serilog.Debugging.SelfLog.Enable(msg => System.Diagnostics.Debug.WriteLine(msg));
//Set this environment variable - so that it can be used in external config file
//add key="serilog:write-to:RollingFile.pathFormat" value="%BASEDIR%\logs\log.txt" />
Environment.SetEnvironmentVariable("BASEDIR", AppDomain.CurrentDomain.BaseDirectory, EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable("MACHINENAME", Environment.MachineName, EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable("UMBLOGDIR", loggingConfiguration.LogDirectory, EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable("BASEDIR", hostingEnvironment.ApplicationPhysicalPath, EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable("MACHINENAME", Environment.MachineName, EnvironmentVariableTarget.Process);
logConfig.MinimumLevel.Verbose() //Set to highest level of logging (as any sinks may want to restrict it to Errors only)
.Enrich.WithProcessId()
.Enrich.WithProcessName()
.Enrich.WithThreadId()
.Enrich.WithProperty(AppDomainId, AppDomain.CurrentDomain.Id)
.Enrich.WithProperty(AppDomainId, AppDomain.CurrentDomain.Id)
.Enrich.WithProperty("AppDomainAppId", hostingEnvironment.ApplicationId.ReplaceNonAlphanumericChars(string.Empty))
.Enrich.WithProperty("MachineName", Environment.MachineName)
.Enrich.With<Log4NetLevelMapperEnricher>()
.Enrich.With(new HttpSessionIdEnricher(sessionIdResolver))
.Enrich.With(new HttpRequestNumberEnricher(requestCacheGetter))
.Enrich.With(new HttpRequestIdEnricher(requestCacheGetter));
.Enrich.FromLogContext(); // allows us to dynamically enrich
return logConfig;
}
@@ -53,13 +54,14 @@ namespace Umbraco.Core.Logging.Serilog
/// Outputs a .txt format log at /App_Data/Logs/
/// </summary>
/// <param name="logConfig">A Serilog LoggerConfiguration</param>
/// <param name="loggingConfiguration"></param>
/// <param name="minimumLevel">The log level you wish the JSON file to collect - default is Verbose (highest)</param>
/// <param name="retainedFileCount">The number of days to keep log files. Default is set to null which means all logs are kept</param>
public static LoggerConfiguration OutputDefaultTextFile(this LoggerConfiguration logConfig, LogEventLevel minimumLevel = LogEventLevel.Verbose, int? retainedFileCount = null)
public static LoggerConfiguration OutputDefaultTextFile(this LoggerConfiguration logConfig, ILoggingConfiguration loggingConfiguration, LogEventLevel minimumLevel = LogEventLevel.Verbose, int? retainedFileCount = null)
{
//Main .txt logfile - in similar format to older Log4Net output
//Ends with ..txt as Date is inserted before file extension substring
logConfig.WriteTo.File($@"{AppDomain.CurrentDomain.BaseDirectory}\App_Data\Logs\UmbracoTraceLog.{Environment.MachineName}..txt",
logConfig.WriteTo.File(Path.Combine(loggingConfiguration.LogDirectory, $@"UmbracoTraceLog.{Environment.MachineName}..txt"),
shared: true,
rollingInterval: RollingInterval.Day,
restrictedToMinimumLevel: minimumLevel,
@@ -99,7 +101,8 @@ namespace Umbraco.Core.Logging.Serilog
rollingInterval,
rollOnFileSizeLimit,
retainedFileCountLimit,
encoding),
encoding,
null),
sinkMapCountLimit:0)
);
}
@@ -109,13 +112,14 @@ namespace Umbraco.Core.Logging.Serilog
/// Outputs a CLEF format JSON log at /App_Data/Logs/
/// </summary>
/// <param name="logConfig">A Serilog LoggerConfiguration</param>
/// <param name="loggingConfiguration"></param>
/// <param name="minimumLevel">The log level you wish the JSON file to collect - default is Verbose (highest)</param>
/// <param name="retainedFileCount">The number of days to keep log files. Default is set to null which means all logs are kept</param>
public static LoggerConfiguration OutputDefaultJsonFile(this LoggerConfiguration logConfig, LogEventLevel minimumLevel = LogEventLevel.Verbose, int? retainedFileCount = null)
public static LoggerConfiguration OutputDefaultJsonFile(this LoggerConfiguration logConfig, ILoggingConfiguration loggingConfiguration, LogEventLevel minimumLevel = LogEventLevel.Verbose, int? retainedFileCount = null)
{
//.clef format (Compact log event format, that can be imported into local SEQ & will make searching/filtering logs easier)
//Ends with ..txt as Date is inserted before file extension substring
logConfig.WriteTo.File(new CompactJsonFormatter(), $@"{AppDomain.CurrentDomain.BaseDirectory}\App_Data\Logs\UmbracoTraceLog.{Environment.MachineName}..json",
logConfig.WriteTo.File(new CompactJsonFormatter(), Path.Combine(loggingConfiguration.LogDirectory, $@"UmbracoTraceLog.{Environment.MachineName}..json"),
shared: true,
rollingInterval: RollingInterval.Day, //Create a new JSON file every day
retainedFileCountLimit: retainedFileCount, //Setting to null means we keep all files - default is 31 days
@@ -129,10 +133,11 @@ namespace Umbraco.Core.Logging.Serilog
/// That allows the main logging pipeline to be configured
/// </summary>
/// <param name="logConfig">A Serilog LoggerConfiguration</param>
public static LoggerConfiguration ReadFromConfigFile(this LoggerConfiguration logConfig)
/// <param name="loggingConfiguration"></param>
public static LoggerConfiguration ReadFromConfigFile(this LoggerConfiguration logConfig, ILoggingConfiguration loggingConfiguration)
{
//Read from main serilog.config file
logConfig.ReadFrom.AppSettings(filePath: AppDomain.CurrentDomain.BaseDirectory + @"\config\serilog.config");
logConfig.ReadFrom.AppSettings(filePath: loggingConfiguration.LogConfigurationFile);
return logConfig;
}
@@ -142,13 +147,15 @@ namespace Umbraco.Core.Logging.Serilog
/// That allows a separate logging pipeline to be configured that will not affect the main Umbraco log
/// </summary>
/// <param name="logConfig">A Serilog LoggerConfiguration</param>
public static LoggerConfiguration ReadFromUserConfigFile(this LoggerConfiguration logConfig)
/// <param name="loggingConfiguration"></param>
public static LoggerConfiguration ReadFromUserConfigFile(this LoggerConfiguration logConfig, ILoggingConfiguration loggingConfiguration)
{
//A nested logger - where any user configured sinks via config can not effect the main 'umbraco' logger above
logConfig.WriteTo.Logger(cfg =>
cfg.ReadFrom.AppSettings(filePath: AppDomain.CurrentDomain.BaseDirectory + @"\config\serilog.user.config"));
cfg.ReadFrom.AppSettings(filePath: loggingConfiguration.UserLogConfigurationFile));
return logConfig;
}
}
}
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Logging.Serilog.Enrichers;
using Umbraco.Infrastructure.Logging.Serilog.Enrichers;
namespace Umbraco.Infrastructure.Logging.Serilog
{
public class SerilogComposer : ICoreComposer
{
public void Compose(Composition composition)
{
composition.RegisterUnique<ThreadAbortExceptionEnricher>();
composition.RegisterUnique<HttpSessionIdEnricher>();
composition.RegisterUnique<HttpRequestNumberEnricher>();
composition.RegisterUnique<HttpRequestIdEnricher>();
}
}
}
@@ -1,72 +1,56 @@
using System;
using System.IO;
using System.Reflection;
using System.Threading;
using Serilog;
using Serilog.Events;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
using Umbraco.Core.Diagnostics;
using Umbraco.Core.Hosting;
using Umbraco.Core.IO;
using Umbraco.Net;
namespace Umbraco.Core.Logging.Serilog
{
///<summary>
/// Implements <see cref="ILogger"/> on top of Serilog.
///</summary>
public class SerilogLogger : ILogger, IDisposable
{
private readonly ICoreDebugSettings _coreDebugSettings;
private readonly IIOHelper _ioHelper;
private readonly IMarchal _marchal;
public global::Serilog.ILogger SerilogLog { get; }
/// <summary>
/// Initialize a new instance of the <see cref="SerilogLogger"/> class with a configuration file.
/// </summary>
/// <param name="logConfigFile"></param>
public SerilogLogger(ICoreDebugSettings coreDebugSettings, IIOHelper ioHelper, IMarchal marchal, FileInfo logConfigFile)
public SerilogLogger(FileInfo logConfigFile)
{
_coreDebugSettings = coreDebugSettings;
_ioHelper = ioHelper;
_marchal = marchal;
Log.Logger = new LoggerConfiguration()
.ReadFrom.AppSettings(filePath: AppDomain.CurrentDomain.BaseDirectory + logConfigFile)
SerilogLog = new LoggerConfiguration()
.ReadFrom.AppSettings(filePath: logConfigFile.FullName)
.CreateLogger();
}
public SerilogLogger(ICoreDebugSettings coreDebugSettings, IIOHelper ioHelper, IMarchal marchal, LoggerConfiguration logConfig)
public SerilogLogger(LoggerConfiguration logConfig)
{
_coreDebugSettings = coreDebugSettings;
_ioHelper = ioHelper;
_marchal = marchal;
//Configure Serilog static global logger with config passed in
Log.Logger = logConfig.CreateLogger();
SerilogLog = logConfig.CreateLogger();
}
/// <summary>
/// Creates a logger with some pre-defined configuration and remainder from config file
/// </summary>
/// <remarks>Used by UmbracoApplicationBase to get its logger.</remarks>
public static SerilogLogger CreateWithDefaultConfiguration(IHostingEnvironment hostingEnvironment, ISessionIdResolver sessionIdResolver, Func<IRequestCache> requestCacheGetter, ICoreDebugSettings coreDebugSettings, IIOHelper ioHelper, IMarchal marchal)
public static SerilogLogger CreateWithDefaultConfiguration(IHostingEnvironment hostingEnvironment, ILoggingConfiguration loggingConfiguration)
{
var loggerConfig = new LoggerConfiguration();
loggerConfig
.MinimalConfiguration(hostingEnvironment, sessionIdResolver, requestCacheGetter)
.ReadFromConfigFile()
.ReadFromUserConfigFile();
.MinimalConfiguration(hostingEnvironment, loggingConfiguration)
.ReadFromConfigFile(loggingConfiguration)
.ReadFromUserConfigFile(loggingConfiguration);
return new SerilogLogger(coreDebugSettings, ioHelper, marchal, loggerConfig);
return new SerilogLogger(loggerConfig);
}
/// <summary>
/// Gets a contextualized logger.
/// </summary>
private global::Serilog.ILogger LoggerFor(Type reporting)
=> Log.Logger.ForContext(reporting);
=> SerilogLog.ForContext(reporting);
/// <summary>
/// Maps Umbraco's log level to Serilog's.
@@ -99,8 +83,7 @@ namespace Umbraco.Core.Logging.Serilog
/// <inheritdoc/>
public void Fatal(Type reporting, Exception exception, string message)
{
var logger = LoggerFor(reporting);
DumpThreadAborts(logger, LogEventLevel.Fatal, exception, ref message);
var logger = LoggerFor(reporting);
logger.Fatal(exception, message);
}
@@ -108,8 +91,7 @@ namespace Umbraco.Core.Logging.Serilog
public void Fatal(Type reporting, Exception exception)
{
var logger = LoggerFor(reporting);
var message = "Exception.";
DumpThreadAborts(logger, LogEventLevel.Fatal, exception, ref message);
var message = "Exception.";
logger.Fatal(exception, message);
}
@@ -128,16 +110,14 @@ namespace Umbraco.Core.Logging.Serilog
/// <inheritdoc/>
public void Fatal(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues)
{
var logger = LoggerFor(reporting);
DumpThreadAborts(logger, LogEventLevel.Fatal, exception, ref messageTemplate);
var logger = LoggerFor(reporting);
logger.Fatal(exception, messageTemplate, propertyValues);
}
/// <inheritdoc/>
public void Error(Type reporting, Exception exception, string message)
{
var logger = LoggerFor(reporting);
DumpThreadAborts(logger, LogEventLevel.Error, exception, ref message);
var logger = LoggerFor(reporting);
logger.Error(exception, message);
}
@@ -146,7 +126,6 @@ namespace Umbraco.Core.Logging.Serilog
{
var logger = LoggerFor(reporting);
var message = "Exception";
DumpThreadAborts(logger, LogEventLevel.Error, exception, ref message);
logger.Error(exception, message);
}
@@ -166,67 +145,9 @@ namespace Umbraco.Core.Logging.Serilog
public void Error(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues)
{
var logger = LoggerFor(reporting);
DumpThreadAborts(logger, LogEventLevel.Error, exception, ref messageTemplate);
logger.Error(exception, messageTemplate, propertyValues);
}
private void DumpThreadAborts(global::Serilog.ILogger logger, LogEventLevel level, Exception exception, ref string messageTemplate)
{
var dump = false;
if (IsTimeoutThreadAbortException(exception))
{
messageTemplate += "\r\nThe thread has been aborted, because the request has timed out.";
// dump if configured, or if stacktrace contains Monitor.ReliableEnter
dump = _coreDebugSettings.DumpOnTimeoutThreadAbort || IsMonitorEnterThreadAbortException(exception);
// dump if it is ok to dump (might have a cap on number of dump...)
dump &= MiniDump.OkToDump(_ioHelper);
}
if (dump)
{
try
{
var dumped = MiniDump.Dump(_marchal, _ioHelper, withException: true);
messageTemplate += dumped
? "\r\nA minidump was created in App_Data/MiniDump"
: "\r\nFailed to create a minidump";
}
catch (Exception ex)
{
messageTemplate += "\r\nFailed to create a minidump";
//Log a new entry (as opposed to appending to same log entry)
logger.Write(level, ex, "Failed to create a minidump ({ExType}: {ExMessage})",
new object[]{ ex.GetType().FullName, ex.Message });
}
}
}
private static bool IsMonitorEnterThreadAbortException(Exception exception)
{
if (!(exception is ThreadAbortException abort)) return false;
var stacktrace = abort.StackTrace;
return stacktrace.Contains("System.Threading.Monitor.ReliableEnter");
}
private static bool IsTimeoutThreadAbortException(Exception exception)
{
if (!(exception is ThreadAbortException abort)) return false;
if (abort.ExceptionState == null) return false;
var stateType = abort.ExceptionState.GetType();
if (stateType.FullName != "System.Web.HttpApplication+CancelModuleException") return false;
var timeoutField = stateType.GetField("_timeout", BindingFlags.Instance | BindingFlags.NonPublic);
if (timeoutField == null) return false;
return (bool) timeoutField.GetValue(abort.ExceptionState);
}
/// <inheritdoc/>
public void Warn(Type reporting, string message)
{
@@ -289,7 +210,7 @@ namespace Umbraco.Core.Logging.Serilog
public void Dispose()
{
Log.CloseAndFlush();
SerilogLog.DisposeIfDisposable();
}
}
}
@@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
namespace Umbraco.Core.Logging.Viewer
{
@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
namespace Umbraco.Core.Logging.Viewer
{
public interface ILogViewerConfig
{
IReadOnlyList<SavedLogSearch> GetSavedSearches();
IReadOnlyList<SavedLogSearch> AddSavedSearch(string name, string query);
IReadOnlyList<SavedLogSearch> DeleteSavedSearch(string name, string query);
}
}
@@ -9,7 +9,8 @@ namespace Umbraco.Core.Logging.Viewer
{
public void Compose(Composition composition)
{
composition.SetLogViewer(factory => new JsonLogViewer(composition.Logger, factory.GetInstance<IIOHelper>()));
composition.RegisterUnique<ILogViewerConfig, LogViewerConfig>();
composition.SetLogViewer<SerilogJsonLogViewer>();
}
}
}
@@ -0,0 +1,84 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using Umbraco.Core.Hosting;
using Formatting = Newtonsoft.Json.Formatting;
namespace Umbraco.Core.Logging.Viewer
{
public class LogViewerConfig : ILogViewerConfig
{
private readonly IHostingEnvironment _hostingEnvironment;
private const string _pathToSearches = "~/Config/logviewer.searches.config.js";
private readonly FileInfo _searchesConfig;
public LogViewerConfig(IHostingEnvironment hostingEnvironment)
{
_hostingEnvironment = hostingEnvironment;
var trimmedPath = _pathToSearches.TrimStart('~', '/').Replace('/', Path.DirectorySeparatorChar);
var absolutePath = Path.Combine(_hostingEnvironment.ApplicationPhysicalPath, trimmedPath);
_searchesConfig = new FileInfo(absolutePath);
}
public IReadOnlyList<SavedLogSearch> GetSavedSearches()
{
//Our default implementation
//If file does not exist - lets create it with an empty array
EnsureFileExists();
var rawJson = System.IO.File.ReadAllText(_searchesConfig.FullName);
return JsonConvert.DeserializeObject<SavedLogSearch[]>(rawJson);
}
public IReadOnlyList<SavedLogSearch> AddSavedSearch(string name, string query)
{
//Get the existing items
var searches = GetSavedSearches().ToList();
//Add the new item to the bottom of the list
searches.Add(new SavedLogSearch { Name = name, Query = query });
//Serialize to JSON string
var rawJson = JsonConvert.SerializeObject(searches, Formatting.Indented);
//If file does not exist - lets create it with an empty array
EnsureFileExists();
//Write it back down to file
System.IO.File.WriteAllText(_searchesConfig.FullName, rawJson);
//Return the updated object - so we can instantly reset the entire array from the API response
//As opposed to push a new item into the array
return searches;
}
public IReadOnlyList<SavedLogSearch> DeleteSavedSearch(string name, string query)
{
//Get the existing items
var searches = GetSavedSearches().ToList();
//Removes the search
searches.RemoveAll(s => s.Name.Equals(name) && s.Query.Equals(query));
//Serialize to JSON string
var rawJson = JsonConvert.SerializeObject(searches, Formatting.Indented);
//Write it back down to file
System.IO.File.WriteAllText(_searchesConfig.FullName, rawJson);
//Return the updated object - so we can instantly reset the entire array from the API response
return searches;
}
private void EnsureFileExists()
{
if (_searchesConfig.Exists) return;
using (var writer = _searchesConfig.CreateText())
{
writer.Write("[]");
}
}
}
}
@@ -5,22 +5,25 @@ using System.Linq;
using Newtonsoft.Json;
using Serilog.Events;
using Serilog.Formatting.Compact.Reader;
using Umbraco.Core.Hosting;
using Umbraco.Core.IO;
namespace Umbraco.Core.Logging.Viewer
{
internal class JsonLogViewer : LogViewerSourceBase
internal class SerilogJsonLogViewer : SerilogLogViewerSourceBase
{
private readonly string _logsPath;
private readonly ILogger _logger;
public JsonLogViewer(ILogger logger, IIOHelper ioHelper, string logsPath = "", string searchPath = "") : base(ioHelper, searchPath)
public SerilogJsonLogViewer(
ILogger logger,
ILogViewerConfig logViewerConfig,
ILoggingConfiguration loggingConfiguration,
global::Serilog.ILogger serilogLog)
: base(logViewerConfig, serilogLog)
{
if (string.IsNullOrEmpty(logsPath))
logsPath = $@"{AppDomain.CurrentDomain.BaseDirectory}\App_Data\Logs\";
_logsPath = logsPath;
_logger = logger;
_logsPath = loggingConfiguration.LogDirectory;
}
private const int FileSizeCap = 100;
@@ -62,9 +65,6 @@ namespace Umbraco.Core.Logging.Viewer
{
var logs = new List<LogEvent>();
//Log Directory
var logDirectory = $@"{AppDomain.CurrentDomain.BaseDirectory}\App_Data\Logs\";
var count = 0;
//foreach full day in the range - see if we can find one or more filenames that end with
@@ -74,7 +74,7 @@ namespace Umbraco.Core.Logging.Viewer
//Filename ending to search for (As could be multiple)
var filesToFind = GetSearchPattern(day);
var filesForCurrentDay = Directory.GetFiles(logDirectory, filesToFind);
var filesForCurrentDay = Directory.GetFiles(_logsPath, filesToFind);
//Foreach file we find - open it
foreach (var filePath in filesForCurrentDay)
@@ -130,7 +130,7 @@ namespace Umbraco.Core.Logging.Viewer
{
// As we are reading/streaming one line at a time in the JSON file
// Thus we can not report the line number, as it will always be 1
_logger.Error<JsonLogViewer>(ex, "Unable to parse a line in the JSON log file");
_logger.Error<SerilogJsonLogViewer>(ex, "Unable to parse a line in the JSON log file");
evt = null;
return true;
@@ -1,31 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using Newtonsoft.Json;
using Serilog;
using Serilog.Events;
using Umbraco.Core.Composing;
using Umbraco.Core.IO;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Formatting = Newtonsoft.Json.Formatting;
namespace Umbraco.Core.Logging.Viewer
{
public abstract class LogViewerSourceBase : ILogViewer
public abstract class SerilogLogViewerSourceBase : ILogViewer
{
private readonly string _searchesConfigPath;
private readonly IIOHelper _ioHelper;
private readonly ILogViewerConfig _logViewerConfig;
private readonly global::Serilog.ILogger _serilogLog;
protected LogViewerSourceBase(IIOHelper ioHelper, string pathToSearches = "")
{
if (string.IsNullOrEmpty(pathToSearches))
// ReSharper disable once StringLiteralTypo
pathToSearches = ioHelper.MapPath("~/Config/logviewer.searches.config.js");
_searchesConfigPath = pathToSearches;
_ioHelper = ioHelper;
protected SerilogLogViewerSourceBase(ILogViewerConfig logViewerConfig, global::Serilog.ILogger serilogLog)
{
_logViewerConfig = logViewerConfig;
_serilogLog = serilogLog;
}
public abstract bool CanHandleLargeLogs { get; }
@@ -38,55 +29,13 @@ namespace Umbraco.Core.Logging.Viewer
public abstract bool CheckCanOpenLogs(LogTimePeriod logTimePeriod);
public virtual IReadOnlyList<SavedLogSearch> GetSavedSearches()
{
//Our default implementation
//If file does not exist - lets create it with an empty array
EnsureFileExists(_searchesConfigPath, "[]", _ioHelper);
var rawJson = System.IO.File.ReadAllText(_searchesConfigPath);
return JsonConvert.DeserializeObject<SavedLogSearch[]>(rawJson);
}
=> _logViewerConfig.GetSavedSearches();
public virtual IReadOnlyList<SavedLogSearch> AddSavedSearch(string name, string query)
{
//Get the existing items
var searches = GetSavedSearches().ToList();
//Add the new item to the bottom of the list
searches.Add(new SavedLogSearch { Name = name, Query = query });
//Serialize to JSON string
var rawJson = JsonConvert.SerializeObject(searches, Formatting.Indented);
//If file does not exist - lets create it with an empty array
EnsureFileExists(_searchesConfigPath, "[]", _ioHelper);
//Write it back down to file
System.IO.File.WriteAllText(_searchesConfigPath, rawJson);
//Return the updated object - so we can instantly reset the entire array from the API response
//As opposed to push a new item into the array
return searches;
}
=> _logViewerConfig.AddSavedSearch(name, query);
public virtual IReadOnlyList<SavedLogSearch> DeleteSavedSearch(string name, string query)
{
//Get the existing items
var searches = GetSavedSearches().ToList();
//Removes the search
searches.RemoveAll(s => s.Name.Equals(name) && s.Query.Equals(query));
//Serialize to JSON string
var rawJson = JsonConvert.SerializeObject(searches, Formatting.Indented);
//Write it back down to file
System.IO.File.WriteAllText(_searchesConfigPath, rawJson);
//Return the updated object - so we can instantly reset the entire array from the API response
return searches;
}
=> _logViewerConfig.DeleteSavedSearch(name, query);
public int GetNumberOfErrors(LogTimePeriod logTimePeriod)
{
@@ -101,7 +50,7 @@ namespace Umbraco.Core.Logging.Viewer
/// <returns></returns>
public string GetLogLevel()
{
var logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast<LogEventLevel>().Where(Log.Logger.IsEnabled)?.Min() ?? null;
var logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast<LogEventLevel>().Where(_serilogLog.IsEnabled)?.Min() ?? null;
return logLevel?.ToString() ?? "";
}
@@ -182,15 +131,6 @@ namespace Umbraco.Core.Logging.Viewer
};
}
private static void EnsureFileExists(string path, string contents, IIOHelper ioHelper)
{
var absolutePath = ioHelper.MapPath(path);
if (System.IO.File.Exists(absolutePath)) return;
using (var writer = System.IO.File.CreateText(absolutePath))
{
writer.Write(contents);
}
}
}
}
@@ -9,7 +9,7 @@ namespace Umbraco.Core.Persistence.Factories
public static IRelationType BuildEntity(RelationTypeDto dto)
{
var entity = new RelationType(dto.Name, dto.Alias, dto.Dual, dto.ChildObjectType, dto.ParentObjectType);
var entity = new RelationType(dto.Name, dto.Alias, dto.Dual, dto.ParentObjectType, dto.ChildObjectType);
try
{
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Entities;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.Querying;
@@ -77,5 +78,7 @@ namespace Umbraco.Core.Persistence.Repositories
/// <remarks>Here, <paramref name="filter"/> can be null but <paramref name="ordering"/> cannot.</remarks>
IEnumerable<TEntity> GetPage(IQuery<TEntity> query, long pageIndex, int pageSize, out long totalRecords,
IQuery<TEntity> filter, Ordering ordering);
ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options);
}
}
@@ -91,7 +91,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
// gets all version ids, current first
public virtual IEnumerable<int> GetVersionIds(int nodeId, int maxRows)
{
var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetVersionIds", tsql =>
var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetVersionIds, tsql =>
tsql.Select<ContentVersionDto>(x => x.Id)
.From<ContentVersionDto>()
.Where<ContentVersionDto>(x => x.NodeId == SqlTemplate.Arg<int>("nodeId"))
@@ -107,7 +107,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
// TODO: test object node type?
// get the version we want to delete
var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetVersion", tsql =>
var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetVersion, tsql =>
tsql.Select<ContentVersionDto>().From<ContentVersionDto>().Where<ContentVersionDto>(x => x.Id == SqlTemplate.Arg<int>("versionId"))
);
var versionDto = Database.Fetch<ContentVersionDto>(template.Sql(new { versionId })).FirstOrDefault();
@@ -129,7 +129,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
// TODO: test object node type?
// get the versions we want to delete, excluding the current one
var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetVersions", tsql =>
var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetVersions, tsql =>
tsql.Select<ContentVersionDto>().From<ContentVersionDto>().Where<ContentVersionDto>(x => x.NodeId == SqlTemplate.Arg<int>("nodeId") && !x.Current && x.VersionDate < SqlTemplate.Arg<DateTime>("versionDate"))
);
var versionDtos = Database.Fetch<ContentVersionDto>(template.Sql(new { nodeId, versionDate }));
@@ -411,7 +411,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
}
// content type alias is invariant
if(ordering.OrderBy.InvariantEquals("contentTypeAlias"))
if (ordering.OrderBy.InvariantEquals("contentTypeAlias"))
{
var joins = Sql()
.InnerJoin<ContentTypeDto>("ctype").On<ContentDto, ContentTypeDto>((content, contentType) => content.ContentTypeId == contentType.NodeId, aliasRight: "ctype");
@@ -485,6 +485,123 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
IQuery<TEntity> filter,
Ordering ordering);
public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options)
{
var report = new Dictionary<int, ContentDataIntegrityReportEntry>();
var sql = SqlContext.Sql()
.Select<NodeDto>()
.From<NodeDto>()
.Where<NodeDto>(x => x.NodeObjectType == NodeObjectTypeId)
.OrderBy<NodeDto>(x => x.Level, x => x.ParentId, x => x.SortOrder);
var nodesToRebuild = new Dictionary<int, List<NodeDto>>();
var validNodes = new Dictionary<int, NodeDto>();
var rootIds = new[] {Constants.System.Root, Constants.System.RecycleBinContent, Constants.System.RecycleBinMedia};
var currentParentIds = new HashSet<int>(rootIds);
var prevParentIds = currentParentIds;
var lastLevel = -1;
// use a forward cursor (query)
foreach (var node in Database.Query<NodeDto>(sql))
{
if (node.Level != lastLevel)
{
// changing levels
prevParentIds = currentParentIds;
currentParentIds = null;
lastLevel = node.Level;
}
if (currentParentIds == null)
{
// we're reset
currentParentIds = new HashSet<int>();
}
currentParentIds.Add(node.NodeId);
// paths parts without the roots
var pathParts = node.Path.Split(',').Where(x => !rootIds.Contains(int.Parse(x))).ToArray();
if (!prevParentIds.Contains(node.ParentId))
{
// invalid, this will be because the level is wrong (which prob means path is wrong too)
report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathAndLevelByParentId));
AppendNodeToFix(nodesToRebuild, node);
}
else if (pathParts.Length == 0)
{
// invalid path
report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathEmpty));
AppendNodeToFix(nodesToRebuild, node);
}
else if (pathParts.Length != node.Level)
{
// invalid, either path or level is wrong
report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathLevelMismatch));
AppendNodeToFix(nodesToRebuild, node);
}
else if (pathParts[pathParts.Length - 1] != node.NodeId.ToString())
{
// invalid path
report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathById));
AppendNodeToFix(nodesToRebuild, node);
}
else if (!rootIds.Contains(node.ParentId) && pathParts[pathParts.Length - 2] != node.ParentId.ToString())
{
// invalid path
report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathByParentId));
AppendNodeToFix(nodesToRebuild, node);
}
else
{
// it's valid!
// don't track unless we are configured to fix
if (options.FixIssues)
validNodes.Add(node.NodeId, node);
}
}
var updated = new List<NodeDto>();
if (options.FixIssues)
{
// iterate all valid nodes to see if these are parents for invalid nodes
foreach (var (nodeId, node) in validNodes)
{
if (!nodesToRebuild.TryGetValue(nodeId, out var invalidNodes)) continue;
// now we can try to rebuild the invalid paths.
foreach (var invalidNode in invalidNodes)
{
invalidNode.Level = (short)(node.Level + 1);
invalidNode.Path = node.Path + "," + invalidNode.NodeId;
updated.Add(invalidNode);
}
}
foreach (var node in updated)
{
Database.Update(node);
if (report.TryGetValue(node.NodeId, out var entry))
entry.Fixed = true;
}
}
return new ContentDataIntegrityReport(report);
}
private static void AppendNodeToFix(IDictionary<int, List<NodeDto>> nodesToRebuild, NodeDto node)
{
if (nodesToRebuild.TryGetValue(node.ParentId, out var childIds))
childIds.Add(node);
else
nodesToRebuild[node.ParentId] = new List<NodeDto> { node };
}
// here, filter can be null and ordering cannot
protected IEnumerable<TEntity> GetPage<TDto>(IQuery<TEntity> query,
long pageIndex, int pageSize, out long totalRecords,
@@ -778,7 +895,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
protected virtual string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0)
{
var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.EnsureUniqueNodeName", tsql => tsql
var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.EnsureUniqueNodeName, tsql => tsql
.Select<NodeDto>(x => Alias(x.NodeId, "id"), x => Alias(x.Text, "name"))
.From<NodeDto>()
.Where<NodeDto>(x => x.NodeObjectType == SqlTemplate.Arg<Guid>("nodeObjectType") && x.ParentId == SqlTemplate.Arg<int>("parentId")));
@@ -791,7 +908,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
protected virtual int GetNewChildSortOrder(int parentId, int first)
{
var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetSortOrder", tsql =>
var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetSortOrder, tsql =>
tsql.Select($"COALESCE(MAX(sortOrder),{first - 1})").From<NodeDto>().Where<NodeDto>(x => x.ParentId == SqlTemplate.Arg<int>("parentId") && x.NodeObjectType == NodeObjectTypeId)
);
@@ -800,7 +917,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
protected virtual NodeDto GetParentNodeDto(int parentId)
{
var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetParentNode", tsql =>
var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetParentNode, tsql =>
tsql.Select<NodeDto>().From<NodeDto>().Where<NodeDto>(x => x.NodeId == SqlTemplate.Arg<int>("parentId"))
);
@@ -809,7 +926,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
protected virtual int GetReservedId(Guid uniqueId)
{
var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetReservedId", tsql =>
var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetReservedId, tsql =>
tsql.Select<NodeDto>(x => x.NodeId).From<NodeDto>().Where<NodeDto>(x => x.UniqueId == SqlTemplate.Arg<Guid>("uniqueId") && x.NodeObjectType == Constants.ObjectTypes.IdReservation)
);
var id = Database.ExecuteScalar<int?>(template.Sql(new { uniqueId = uniqueId }));
@@ -331,7 +331,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
.InnerJoin<DocumentVersionDto>()
.On<ContentVersionDto, DocumentVersionDto>((c, d) => c.Id == d.Id)
.Where<ContentVersionDto>(x => x.NodeId == SqlTemplate.Arg<int>("nodeId") && !x.Current && x.VersionDate < SqlTemplate.Arg<DateTime>("versionDate"))
.Where<DocumentVersionDto>( x => !x.Published)
.Where<DocumentVersionDto>(x => !x.Published)
);
var versionDtos = Database.Fetch<ContentVersionDto>(template.Sql(new { nodeId, versionDate }));
foreach (var versionDto in versionDtos)
@@ -529,8 +529,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
protected override void PersistUpdatedItem(IContent entity)
{
var entityBase = entity as EntityBase;
var isEntityDirty = entityBase != null && entityBase.IsDirty();
var isEntityDirty = entity.IsDirty();
// check if we need to make any database changes at all
if ((entity.PublishedState == PublishedState.Published || entity.PublishedState == PublishedState.Unpublished)
@@ -545,29 +544,41 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
// update
entity.UpdatingEntity();
// Check if this entity is being moved as a descendant as part of a bulk moving operations.
// In this case we can bypass a lot of the below operations which will make this whole operation go much faster.
// When moving we don't need to create new versions, etc... because we cannot roll this operation back anyways.
var isMoving = entity.IsMoving();
// TODO: I'm sure we can also detect a "Copy" (of a descendant) operation and probably perform similar checks below.
// There is probably more stuff that would be required for copying but I'm sure not all of this logic would be, we could more than likely boost
// copy performance by 95% just like we did for Move
var publishing = entity.PublishedState == PublishedState.Publishing;
// check if we need to create a new version
if (publishing && entity.PublishedVersionId > 0)
if (!isMoving)
{
// published version is not published anymore
Database.Execute(Sql().Update<DocumentVersionDto>(u => u.Set(x => x.Published, false)).Where<DocumentVersionDto>(x => x.Id == entity.PublishedVersionId));
}
// check if we need to create a new version
if (publishing && entity.PublishedVersionId > 0)
{
// published version is not published anymore
Database.Execute(Sql().Update<DocumentVersionDto>(u => u.Set(x => x.Published, false)).Where<DocumentVersionDto>(x => x.Id == entity.PublishedVersionId));
}
// sanitize names
SanitizeNames(entity, publishing);
// sanitize names
SanitizeNames(entity, publishing);
// ensure that strings don't contain characters that are invalid in xml
// TODO: do we really want to keep doing this here?
entity.SanitizeEntityPropertiesForXmlStorage();
// ensure that strings don't contain characters that are invalid in xml
// TODO: do we really want to keep doing this here?
entity.SanitizeEntityPropertiesForXmlStorage();
// if parent has changed, get path, level and sort order
if (entity.IsPropertyDirty("ParentId"))
{
var parent = GetParentNodeDto(entity.ParentId);
entity.Path = string.Concat(parent.Path, ",", entity.Id);
entity.Level = parent.Level + 1;
entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0);
// if parent has changed, get path, level and sort order
if (entity.IsPropertyDirty("ParentId"))
{
var parent = GetParentNodeDto(entity.ParentId);
entity.Path = string.Concat(parent.Path, ",", entity.Id);
entity.Level = parent.Level + 1;
entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0);
}
}
// create the dto
@@ -578,146 +589,152 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
nodeDto.ValidatePathWithException();
Database.Update(nodeDto);
// update the content dto
Database.Update(dto.ContentDto);
// update the content & document version dtos
var contentVersionDto = dto.DocumentVersionDto.ContentVersionDto;
var documentVersionDto = dto.DocumentVersionDto;
if (publishing)
if (!isMoving)
{
documentVersionDto.Published = true; // now published
contentVersionDto.Current = false; // no more current
}
Database.Update(contentVersionDto);
Database.Update(documentVersionDto);
// update the content dto
Database.Update(dto.ContentDto);
// and, if publishing, insert new content & document version dtos
if (publishing)
{
entity.PublishedVersionId = entity.VersionId;
contentVersionDto.Id = 0; // want a new id
contentVersionDto.Current = true; // current version
contentVersionDto.Text = entity.Name;
Database.Insert(contentVersionDto);
entity.VersionId = documentVersionDto.Id = contentVersionDto.Id; // get the new id
documentVersionDto.Published = false; // non-published version
Database.Insert(documentVersionDto);
}
// replace the property data (rather than updating)
// only need to delete for the version that existed, the new version (if any) has no property data yet
var versionToDelete = publishing ? entity.PublishedVersionId : entity.VersionId;
var deletePropertyDataSql = Sql().Delete<PropertyDataDto>().Where<PropertyDataDto>(x => x.VersionId == versionToDelete);
Database.Execute(deletePropertyDataSql);
// insert property data
var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishing ? entity.PublishedVersionId : 0,
entity.Properties, LanguageRepository, out var edited, out var editedCultures);
foreach (var propertyDataDto in propertyDataDtos)
Database.Insert(propertyDataDto);
// if !publishing, we may have a new name != current publish name,
// also impacts 'edited'
if (!publishing && entity.PublishName != entity.Name)
edited = true;
if (entity.ContentType.VariesByCulture())
{
// bump dates to align cultures to version
// update the content & document version dtos
var contentVersionDto = dto.DocumentVersionDto.ContentVersionDto;
var documentVersionDto = dto.DocumentVersionDto;
if (publishing)
entity.AdjustDates(contentVersionDto.VersionDate);
{
documentVersionDto.Published = true; // now published
contentVersionDto.Current = false; // no more current
}
Database.Update(contentVersionDto);
Database.Update(documentVersionDto);
// names also impact 'edited'
// ReSharper disable once UseDeconstruction
foreach (var cultureInfo in entity.CultureInfos)
if (cultureInfo.Name != entity.GetPublishName(cultureInfo.Culture))
{
edited = true;
(editedCultures ?? (editedCultures = new HashSet<string>(StringComparer.OrdinalIgnoreCase))).Add(cultureInfo.Culture);
// and, if publishing, insert new content & document version dtos
if (publishing)
{
entity.PublishedVersionId = entity.VersionId;
// TODO: change tracking
// at the moment, we don't do any dirty tracking on property values, so we don't know whether the
// culture has just been edited or not, so we don't update its update date - that date only changes
// when the name is set, and it all works because the controller does it - but, if someone uses a
// service to change a property value and save (without setting name), the update date does not change.
}
contentVersionDto.Id = 0; // want a new id
contentVersionDto.Current = true; // current version
contentVersionDto.Text = entity.Name;
Database.Insert(contentVersionDto);
entity.VersionId = documentVersionDto.Id = contentVersionDto.Id; // get the new id
// replace the content version variations (rather than updating)
documentVersionDto.Published = false; // non-published version
Database.Insert(documentVersionDto);
}
// replace the property data (rather than updating)
// only need to delete for the version that existed, the new version (if any) has no property data yet
var deleteContentVariations = Sql().Delete<ContentVersionCultureVariationDto>().Where<ContentVersionCultureVariationDto>(x => x.VersionId == versionToDelete);
Database.Execute(deleteContentVariations);
var versionToDelete = publishing ? entity.PublishedVersionId : entity.VersionId;
var deletePropertyDataSql = Sql().Delete<PropertyDataDto>().Where<PropertyDataDto>(x => x.VersionId == versionToDelete);
Database.Execute(deletePropertyDataSql);
// replace the document version variations (rather than updating)
var deleteDocumentVariations = Sql().Delete<DocumentCultureVariationDto>().Where<DocumentCultureVariationDto>(x => x.NodeId == entity.Id);
Database.Execute(deleteDocumentVariations);
// insert property data
var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishing ? entity.PublishedVersionId : 0,
entity.Properties, LanguageRepository, out var edited, out var editedCultures);
foreach (var propertyDataDto in propertyDataDtos)
Database.Insert(propertyDataDto);
// TODO: NPoco InsertBulk issue?
// we should use the native NPoco InsertBulk here but it causes problems (not sure exactly all scenarios)
// but by using SQL Server and updating a variants name will cause: Unable to cast object of type
// 'Umbraco.Core.Persistence.FaultHandling.RetryDbConnection' to type 'System.Data.SqlClient.SqlConnection'.
// (same in PersistNewItem above)
// if !publishing, we may have a new name != current publish name,
// also impacts 'edited'
if (!publishing && entity.PublishName != entity.Name)
edited = true;
// insert content variations
Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing));
if (entity.ContentType.VariesByCulture())
{
// bump dates to align cultures to version
if (publishing)
entity.AdjustDates(contentVersionDto.VersionDate);
// insert document variations
Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures));
// names also impact 'edited'
// ReSharper disable once UseDeconstruction
foreach (var cultureInfo in entity.CultureInfos)
if (cultureInfo.Name != entity.GetPublishName(cultureInfo.Culture))
{
edited = true;
(editedCultures ?? (editedCultures = new HashSet<string>(StringComparer.OrdinalIgnoreCase))).Add(cultureInfo.Culture);
// TODO: change tracking
// at the moment, we don't do any dirty tracking on property values, so we don't know whether the
// culture has just been edited or not, so we don't update its update date - that date only changes
// when the name is set, and it all works because the controller does it - but, if someone uses a
// service to change a property value and save (without setting name), the update date does not change.
}
// replace the content version variations (rather than updating)
// only need to delete for the version that existed, the new version (if any) has no property data yet
var deleteContentVariations = Sql().Delete<ContentVersionCultureVariationDto>().Where<ContentVersionCultureVariationDto>(x => x.VersionId == versionToDelete);
Database.Execute(deleteContentVariations);
// replace the document version variations (rather than updating)
var deleteDocumentVariations = Sql().Delete<DocumentCultureVariationDto>().Where<DocumentCultureVariationDto>(x => x.NodeId == entity.Id);
Database.Execute(deleteDocumentVariations);
// TODO: NPoco InsertBulk issue?
// we should use the native NPoco InsertBulk here but it causes problems (not sure exactly all scenarios)
// but by using SQL Server and updating a variants name will cause: Unable to cast object of type
// 'Umbraco.Core.Persistence.FaultHandling.RetryDbConnection' to type 'System.Data.SqlClient.SqlConnection'.
// (same in PersistNewItem above)
// insert content variations
Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing));
// insert document variations
Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures));
}
// refresh content
entity.SetCultureEdited(editedCultures);
// update the document dto
// at that point, when un/publishing, the entity still has its old Published value
// so we need to explicitly update the dto to persist the correct value
if (entity.PublishedState == PublishedState.Publishing)
dto.Published = true;
else if (entity.PublishedState == PublishedState.Unpublishing)
dto.Published = false;
entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited
Database.Update(dto);
//update the schedule
if (entity.IsPropertyDirty("ContentSchedule"))
PersistContentSchedule(entity, true);
// if entity is publishing, update tags, else leave tags there
// means that implicitly unpublished, or trashed, entities *still* have tags in db
if (entity.PublishedState == PublishedState.Publishing)
SetEntityTags(entity, _tagRepository);
}
// refresh content
entity.SetCultureEdited(editedCultures);
// update the document dto
// at that point, when un/publishing, the entity still has its old Published value
// so we need to explicitly update the dto to persist the correct value
if (entity.PublishedState == PublishedState.Publishing)
dto.Published = true;
else if (entity.PublishedState == PublishedState.Unpublishing)
dto.Published = false;
entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited
Database.Update(dto);
//update the schedule
if (entity.IsPropertyDirty("ContentSchedule"))
PersistContentSchedule(entity, true);
// if entity is publishing, update tags, else leave tags there
// means that implicitly unpublished, or trashed, entities *still* have tags in db
if (entity.PublishedState == PublishedState.Publishing)
SetEntityTags(entity, _tagRepository);
// trigger here, before we reset Published etc
OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity));
// flip the entity's published property
// this also flips its published state
if (entity.PublishedState == PublishedState.Publishing)
if (!isMoving)
{
entity.Published = true;
entity.PublishTemplateId = entity.TemplateId;
entity.PublisherId = entity.WriterId;
entity.PublishName = entity.Name;
entity.PublishDate = entity.UpdateDate;
// flip the entity's published property
// this also flips its published state
if (entity.PublishedState == PublishedState.Publishing)
{
entity.Published = true;
entity.PublishTemplateId = entity.TemplateId;
entity.PublisherId = entity.WriterId;
entity.PublishName = entity.Name;
entity.PublishDate = entity.UpdateDate;
SetEntityTags(entity, _tagRepository);
SetEntityTags(entity, _tagRepository);
}
else if (entity.PublishedState == PublishedState.Unpublishing)
{
entity.Published = false;
entity.PublishTemplateId = null;
entity.PublisherId = null;
entity.PublishName = null;
entity.PublishDate = null;
ClearEntityTags(entity, _tagRepository);
}
PersistRelations(entity);
// TODO: note re. tags: explicitly unpublished entities have cleared tags, but masked or trashed entities *still* have tags in the db - so what?
}
else if (entity.PublishedState == PublishedState.Unpublishing)
{
entity.Published = false;
entity.PublishTemplateId = null;
entity.PublisherId = null;
entity.PublishName = null;
entity.PublishDate = null;
ClearEntityTags(entity, _tagRepository);
}
PersistRelations(entity);
// TODO: note re. tags: explicitly unpublished entities have cleared tags, but masked or trashed entities *still* have tags in the db - so what?
entity.ResetDirtyProperties();
@@ -231,7 +231,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
protected override void PersistNewItem(IMedia entity)
{
var media = (Models.Media) entity;
entity.AddingEntity();
// ensure unique name on the same level
@@ -286,15 +285,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
contentVersionDto.NodeId = nodeDto.NodeId;
contentVersionDto.Current = true;
Database.Insert(contentVersionDto);
media.VersionId = contentVersionDto.Id;
entity.VersionId = contentVersionDto.Id;
// persist the media version dto
var mediaVersionDto = dto.MediaVersionDto;
mediaVersionDto.Id = media.VersionId;
mediaVersionDto.Id = entity.VersionId;
Database.Insert(mediaVersionDto);
// persist the property data
var propertyDataDtos = PropertyFactory.BuildDtos(media.ContentType.Variations, media.VersionId, 0, entity.Properties, LanguageRepository, out _, out _);
var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, 0, entity.Properties, LanguageRepository, out _, out _);
foreach (var propertyDataDto in propertyDataDtos)
Database.Insert(propertyDataDto);
@@ -310,26 +309,32 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
protected override void PersistUpdatedItem(IMedia entity)
{
var media = (Models.Media) entity;
// update
media.UpdatingEntity();
entity.UpdatingEntity();
// ensure unique name on the same level
entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id);
// Check if this entity is being moved as a descendant as part of a bulk moving operations.
// In this case we can bypass a lot of the below operations which will make this whole operation go much faster.
// When moving we don't need to create new versions, etc... because we cannot roll this operation back anyways.
var isMoving = entity.IsMoving();
// ensure that strings don't contain characters that are invalid in xml
// TODO: do we really want to keep doing this here?
entity.SanitizeEntityPropertiesForXmlStorage();
// if parent has changed, get path, level and sort order
if (entity.IsPropertyDirty("ParentId"))
if (!isMoving)
{
var parent = GetParentNodeDto(entity.ParentId);
// ensure unique name on the same level
entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id);
entity.Path = string.Concat(parent.Path, ",", entity.Id);
entity.Level = parent.Level + 1;
entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0);
// ensure that strings don't contain characters that are invalid in xml
// TODO: do we really want to keep doing this here?
entity.SanitizeEntityPropertiesForXmlStorage();
// if parent has changed, get path, level and sort order
if (entity.IsPropertyDirty(nameof(entity.ParentId)))
{
var parent = GetParentNodeDto(entity.ParentId);
entity.Path = string.Concat(parent.Path, ",", entity.Id);
entity.Level = parent.Level + 1;
entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0);
}
}
// create the dto
@@ -340,26 +345,29 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
nodeDto.ValidatePathWithException();
Database.Update(nodeDto);
// update the content dto
Database.Update(dto.ContentDto);
if (!isMoving)
{
// update the content dto
Database.Update(dto.ContentDto);
// update the content & media version dtos
var contentVersionDto = dto.MediaVersionDto.ContentVersionDto;
var mediaVersionDto = dto.MediaVersionDto;
contentVersionDto.Current = true;
Database.Update(contentVersionDto);
Database.Update(mediaVersionDto);
// update the content & media version dtos
var contentVersionDto = dto.MediaVersionDto.ContentVersionDto;
var mediaVersionDto = dto.MediaVersionDto;
contentVersionDto.Current = true;
Database.Update(contentVersionDto);
Database.Update(mediaVersionDto);
// replace the property data
var deletePropertyDataSql = SqlContext.Sql().Delete<PropertyDataDto>().Where<PropertyDataDto>(x => x.VersionId == media.VersionId);
Database.Execute(deletePropertyDataSql);
var propertyDataDtos = PropertyFactory.BuildDtos(media.ContentType.Variations, media.VersionId, 0, entity.Properties, LanguageRepository, out _, out _);
foreach (var propertyDataDto in propertyDataDtos)
Database.Insert(propertyDataDto);
// replace the property data
var deletePropertyDataSql = SqlContext.Sql().Delete<PropertyDataDto>().Where<PropertyDataDto>(x => x.VersionId == entity.VersionId);
Database.Execute(deletePropertyDataSql);
var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, 0, entity.Properties, LanguageRepository, out _, out _);
foreach (var propertyDataDto in propertyDataDtos)
Database.Insert(propertyDataDto);
SetEntityTags(entity, _tagRepository);
SetEntityTags(entity, _tagRepository);
PersistRelations(entity);
PersistRelations(entity);
}
OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity));
@@ -23,7 +23,6 @@ namespace Umbraco.Core.PropertyEditors
public class DataEditor : IDataEditor
{
private IDictionary<string, object> _defaultConfiguration;
private IDataValueEditor _dataValueEditor;
/// <summary>
/// Initializes a new instance of the <see cref="DataEditor"/> class.
@@ -105,7 +104,7 @@ namespace Umbraco.Core.PropertyEditors
/// simple enough for now.</para>
/// </remarks>
// TODO: point of that one? shouldn't we always configure?
public IDataValueEditor GetValueEditor() => ExplicitValueEditor ?? (_dataValueEditor ?? (_dataValueEditor = CreateValueEditor()));
public IDataValueEditor GetValueEditor() => ExplicitValueEditor ?? CreateValueEditor();
/// <inheritdoc />
/// <remarks>
@@ -186,6 +186,10 @@ namespace Umbraco.Web.PropertyEditors
public IEnumerable<UmbracoEntityReference> GetReferences(object value)
{
var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString();
if (rawJson.IsNullOrWhiteSpace())
yield break;
DeserializeGridValue(rawJson, out var richTextEditorValues, out var mediaValues);
foreach (var umbracoEntityReference in richTextEditorValues.SelectMany(x =>
@@ -57,9 +57,11 @@ namespace Umbraco.Web.PropertyEditors
if (string.IsNullOrEmpty(asString)) yield break;
if (UdiParser.TryParse(asString, out var udi))
yield return new UmbracoEntityReference(udi);
foreach (var udiStr in asString.Split(','))
{
if (UdiParser.TryParse(udiStr, out var udi))
yield return new UmbracoEntityReference(udi);
}
}
}
}
@@ -1,4 +1,5 @@
using System;
using Examine;
using Umbraco.Core.Cache;
using Umbraco.Core.Composing;
using Umbraco.Core.Composing.CompositionExtensions;
@@ -7,9 +8,11 @@ using Umbraco.Core.Configuration.Grid;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Dashboards;
using Umbraco.Core.Dictionary;
using Umbraco.Core.Events;
using Umbraco.Core.Hosting;
using Umbraco.Core.Logging;
using Umbraco.Core.Manifest;
using Umbraco.Core.Media;
using Umbraco.Core.Migrations;
using Umbraco.Core.Migrations.Install;
using Umbraco.Core.Migrations.PostMigrations;
@@ -17,19 +20,36 @@ using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Persistence;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.PropertyEditors.Validators;
using Umbraco.Core.PropertyEditors.ValueConverters;
using Umbraco.Core.Scoping;
using Umbraco.Core.Serialization;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement;
using Umbraco.Core.Strings;
using Umbraco.Core.Sync;
using Umbraco.Examine;
using Umbraco.Infrastructure.Media;
using Umbraco.Net;
using Umbraco.Web;
using Umbraco.Web.Actions;
using Umbraco.Web.Cache;
using Umbraco.Web.ContentApps;
using Umbraco.Web.Editors;
using Umbraco.Web.Features;
using Umbraco.Web.HealthCheck;
using Umbraco.Web.HealthCheck.NotificationMethods;
using Umbraco.Web.Install;
using Umbraco.Web.Macros;
using Umbraco.Web.Media.EmbedProviders;
using Umbraco.Web.Migrations.PostMigrations;
using Umbraco.Web.Models.PublishedContent;
using Umbraco.Web.PropertyEditors;
using Umbraco.Web.PublishedCache;
using Umbraco.Web.Routing;
using Umbraco.Web.Search;
using Umbraco.Web.Sections;
using Umbraco.Web.Services;
using Umbraco.Web.Templates;
using Umbraco.Web.Trees;
using IntegerValidator = Umbraco.Core.PropertyEditors.Validators.IntegerValidator;
@@ -42,7 +62,7 @@ namespace Umbraco.Core.Runtime
public override void Compose(Composition composition)
{
base.Compose(composition);
// composers
composition
.ComposeRepositories()
@@ -177,6 +197,161 @@ namespace Umbraco.Core.Runtime
// Config manipulator
composition.RegisterUnique<IConfigManipulator, JsonConfigManipulator>();
// register the http context and umbraco context accessors
// we *should* use the HttpContextUmbracoContextAccessor, however there are cases when
// we have no http context, eg when booting Umbraco or in background threads, so instead
// let's use an hybrid accessor that can fall back to a ThreadStatic context.
composition.RegisterUnique<IUmbracoContextAccessor, HybridUmbracoContextAccessor>();
// register the umbraco context factory
// composition.RegisterUnique<IUmbracoContextFactory, UmbracoContextFactory>();
composition.RegisterUnique<IPublishedUrlProvider, UrlProvider>();
composition.RegisterUnique<HtmlLocalLinkParser>();
composition.RegisterUnique<HtmlImageSourceParser>();
composition.RegisterUnique<HtmlUrlParser>();
composition.RegisterUnique<RichTextEditorPastedImages>();
// both TinyMceValueConverter (in Core) and RteMacroRenderingValueConverter (in Web) will be
// discovered when CoreBootManager configures the converters. We HAVE to remove one of them
// here because there cannot be two converters for one property editor - and we want the full
// RteMacroRenderingValueConverter that converts macros, etc. So remove TinyMceValueConverter.
// (the limited one, defined in Core, is there for tests) - same for others
composition.PropertyValueConverters()
.Remove<TinyMceValueConverter>()
.Remove<TextStringValueConverter>()
.Remove<MarkdownEditorValueConverter>();
composition.UrlProviders()
.Append<AliasUrlProvider>()
.Append<DefaultUrlProvider>();
composition.MediaUrlProviders()
.Append<DefaultMediaUrlProvider>();
composition.RegisterUnique<ISiteDomainHelper, SiteDomainHelper>();
// register properties fallback
composition.RegisterUnique<IPublishedValueFallback, PublishedValueFallback>();
composition.RegisterUnique<IImageUrlGenerator, ImageSharpImageUrlGenerator>();
composition.RegisterUnique<UmbracoFeatures>();
composition.Actions()
.Add(() => composition.TypeLoader.GetTypes<IAction>());
composition.EditorValidators()
.Add(() => composition.TypeLoader.GetTypes<IEditorValidator>());
composition.TourFilters();
// replace with web implementation
composition.RegisterUnique<IPublishedSnapshotRebuilder, PublishedSnapshotRebuilder>();
// register OEmbed providers - no type scanning - all explicit opt-in of adding types
// note: IEmbedProvider is not IDiscoverable - think about it if going for type scanning
composition.OEmbedProviders()
.Append<YouTube>()
.Append<Instagram>()
.Append<Twitter>()
.Append<Vimeo>()
.Append<DailyMotion>()
.Append<Flickr>()
.Append<Slideshare>()
.Append<Kickstarter>()
.Append<GettyImages>()
.Append<Ted>()
.Append<Soundcloud>()
.Append<Issuu>()
.Append<Hulu>()
.Append<Giphy>();
// register back office sections in the order we want them rendered
composition.Sections()
.Append<ContentSection>()
.Append<MediaSection>()
.Append<SettingsSection>()
.Append<PackagesSection>()
.Append<UsersSection>()
.Append<MembersSection>()
.Append<FormsSection>()
.Append<TranslationSection>();
// register known content apps
composition.ContentApps()
.Append<ListViewContentAppFactory>()
.Append<ContentEditorContentAppFactory>()
.Append<ContentInfoContentAppFactory>();
// register published router
composition.RegisterUnique<IPublishedRouter, PublishedRouter>();
// register *all* checks, except those marked [HideFromTypeFinder] of course
composition.HealthChecks()
.Add(() => composition.TypeLoader.GetTypes<HealthCheck>());
composition.WithCollectionBuilder<HealthCheckNotificationMethodCollectionBuilder>()
.Add(() => composition.TypeLoader.GetTypes<IHealthCheckNotificationMethod>());
composition.RegisterUnique<IContentLastChanceFinder, ContentFinderByConfigured404>();
composition.ContentFinders()
// all built-in finders in the correct order,
// devs can then modify this list on application startup
.Append<ContentFinderByPageIdQuery>()
.Append<ContentFinderByUrl>()
.Append<ContentFinderByIdPath>()
//.Append<ContentFinderByUrlAndTemplate>() // disabled, this is an odd finder
.Append<ContentFinderByUrlAlias>()
.Append<ContentFinderByRedirectUrl>();
composition.Register<UmbracoTreeSearcher>(Lifetime.Request);
composition.SearchableTrees()
.Add(() => composition.TypeLoader.GetTypes<ISearchableTree>());
// replace some services
composition.RegisterUnique<IEventMessagesFactory, DefaultEventMessagesFactory>();
composition.RegisterUnique<IEventMessagesAccessor, HybridEventMessagesAccessor>();
composition.RegisterUnique<ITreeService, TreeService>();
composition.RegisterUnique<ISectionService, SectionService>();
composition.RegisterUnique<IExamineManager, ExamineManager>();
// register distributed cache
composition.RegisterUnique(f => new DistributedCache(f.GetInstance<IServerMessenger>(), f.GetInstance<CacheRefresherCollection>()));
composition.Register<ITagQuery, TagQuery>(Lifetime.Request);
composition.RegisterUnique<HtmlLocalLinkParser>();
composition.RegisterUnique<HtmlUrlParser>();
composition.RegisterUnique<HtmlImageSourceParser>();
composition.RegisterUnique<RichTextEditorPastedImages>();
composition.RegisterUnique<IUmbracoTreeSearcherFields, UmbracoTreeSearcherFields>();
composition.Register<IPublishedContentQuery>(factory =>
{
var umbCtx = factory.GetInstance<IUmbracoContextAccessor>();
return new PublishedContentQuery(umbCtx.UmbracoContext.PublishedSnapshot, factory.GetInstance<IVariationContextAccessor>(), factory.GetInstance<IExamineManager>());
}, Lifetime.Request);
composition.RegisterUnique<IPublishedUrlProvider, UrlProvider>();
// register the http context and umbraco context accessors
// we *should* use the HttpContextUmbracoContextAccessor, however there are cases when
// we have no http context, eg when booting Umbraco or in background threads, so instead
// let's use an hybrid accessor that can fall back to a ThreadStatic context.
composition.RegisterUnique<IUmbracoContextAccessor, HybridUmbracoContextAccessor>();
// register accessors for cultures
composition.RegisterUnique<IDefaultCultureAccessor, DefaultCultureAccessor>();
}
}
}
@@ -25,6 +25,7 @@ namespace Umbraco.Core.Runtime
private IFactory _factory;
private readonly RuntimeState _state;
private readonly IUmbracoBootPermissionChecker _umbracoBootPermissionChecker;
private readonly IRequestCache _requestCache;
private readonly IGlobalSettings _globalSettings;
private readonly IConnectionStrings _connectionStrings;
@@ -39,7 +40,8 @@ namespace Umbraco.Core.Runtime
IBackOfficeInfo backOfficeInfo,
IDbProviderFactoryCreator dbProviderFactoryCreator,
IMainDom mainDom,
ITypeFinder typeFinder)
ITypeFinder typeFinder,
IRequestCache requestCache)
{
IOHelper = ioHelper;
Configs = configs;
@@ -50,6 +52,7 @@ namespace Umbraco.Core.Runtime
DbProviderFactoryCreator = dbProviderFactoryCreator;
_umbracoBootPermissionChecker = umbracoBootPermissionChecker;
_requestCache = requestCache;
Logger = logger;
MainDom = mainDom;
@@ -110,6 +113,7 @@ namespace Umbraco.Core.Runtime
{
if (register is null) throw new ArgumentNullException(nameof(register));
// create and register the essential services
// ie the bare minimum required to boot
@@ -129,12 +133,19 @@ namespace Umbraco.Core.Runtime
"Booted.",
"Boot failed."))
{
Logger.Info<CoreRuntime>("Booting Core");
Logger.Info<CoreRuntime>("Booting site '{HostingSiteName}', app '{HostingApplicationId}', path '{HostingPhysicalPath}', server '{MachineName}'.",
HostingEnvironment?.SiteName,
HostingEnvironment?.ApplicationId,
HostingEnvironment?.ApplicationPhysicalPath,
NetworkHelper.MachineName);
Logger.Debug<CoreRuntime>("Runtime: {Runtime}", GetType().FullName);
// application environment
ConfigureUnhandledException();
return _factory = Configure(register, timer);
_factory = Configure(register, timer);
return _factory;
}
}
@@ -151,7 +162,7 @@ namespace Umbraco.Core.Runtime
try
{
// run handlers
RuntimeOptions.DoRuntimeBoot(ProfilingLogger);
@@ -244,6 +255,13 @@ namespace Umbraco.Core.Runtime
// create & initialize the components
_components = _factory.GetInstance<ComponentCollection>();
_components.Initialize();
// now (and only now) is the time to switch over to perWebRequest scopes.
// up until that point we may not have a request, and scoped services would
// fail to resolve - but we run Initialize within a factory scope - and then,
// here, we switch the factory to bind scopes to requests
_factory.EnablePerWebRequestScope();
}
protected virtual void ConfigureUnhandledException()
@@ -350,7 +368,7 @@ namespace Umbraco.Core.Runtime
return new AppCaches(
new DeepCloneAppCache(new ObjectCacheAppCache()),
NoAppCache.Instance,
_requestCache,
new IsolatedCaches(type => new DeepCloneAppCache(new ObjectCacheAppCache())));
}
@@ -1,90 +0,0 @@
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration;
using Umbraco.Core.Hosting;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Persistence;
using Umbraco.Core.Runtime;
namespace Umbraco.Web.Runtime
{
/// <summary>
/// Represents the Web Umbraco runtime.
/// </summary>
/// <remarks>On top of CoreRuntime, handles all of the web-related runtime aspects of Umbraco.</remarks>
public class WebRuntime : CoreRuntime
{
private readonly IRequestCache _requestCache;
/// <summary>
/// Initializes a new instance of the <see cref="WebRuntime"/> class.
/// </summary>
public WebRuntime(
Configs configs,
IUmbracoVersion umbracoVersion,
IIOHelper ioHelper,
ILogger logger,
IProfiler profiler,
IHostingEnvironment hostingEnvironment,
IBackOfficeInfo backOfficeInfo,
IDbProviderFactoryCreator dbProviderFactoryCreator,
IMainDom mainDom,
ITypeFinder typeFinder,
IRequestCache requestCache,
IUmbracoBootPermissionChecker umbracoBootPermissionChecker):
base(configs, umbracoVersion, ioHelper, logger, profiler ,umbracoBootPermissionChecker, hostingEnvironment, backOfficeInfo, dbProviderFactoryCreator, mainDom, typeFinder)
{
_requestCache = requestCache;
}
/// <inheritdoc/>
public override IFactory Configure(IRegister register)
{
var profilingLogger = new ProfilingLogger(Logger, Profiler);
var umbracoVersion = new UmbracoVersion();
using (var timer = profilingLogger.TraceDuration<CoreRuntime>(
$"Booting Umbraco {umbracoVersion.SemanticVersion.ToSemanticString()}.",
"Booted.",
"Boot failed."))
{
Logger.Info<CoreRuntime>("Booting site '{HostingSiteName}', app '{HostingApplicationId}', path '{HostingPhysicalPath}', server '{MachineName}'.",
HostingEnvironment.SiteName,
HostingEnvironment.ApplicationId,
HostingEnvironment.ApplicationPhysicalPath,
NetworkHelper.MachineName);
Logger.Debug<CoreRuntime>("Runtime: {Runtime}", GetType().FullName);
var factory = base.Configure(register);
// now (and only now) is the time to switch over to perWebRequest scopes.
// up until that point we may not have a request, and scoped services would
// fail to resolve - but we run Initialize within a factory scope - and then,
// here, we switch the factory to bind scopes to requests
factory.EnablePerWebRequestScope();
return factory;
}
}
#region Getters
protected override AppCaches GetAppCaches() => new AppCaches(
// we need to have the dep clone runtime cache provider to ensure
// all entities are cached properly (cloned in and cloned out)
new DeepCloneAppCache(new ObjectCacheAppCache()),
// we need request based cache when running in web-based context
_requestCache,
new IsolatedCaches(type =>
// we need to have the dep clone runtime cache provider to ensure
// all entities are cached properly (cloned in and cloned out)
new DeepCloneAppCache(new ObjectCacheAppCache())));
#endregion
}
}
@@ -12,6 +12,7 @@ using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services.Changes;
using Umbraco.Core.Strings;
namespace Umbraco.Core.Services.Implement
{
@@ -27,6 +28,7 @@ namespace Umbraco.Core.Services.Implement
private readonly IDocumentBlueprintRepository _documentBlueprintRepository;
private readonly ILanguageRepository _languageRepository;
private readonly Lazy<IPropertyValidationService> _propertyValidationService;
private readonly IShortStringHelper _shortStringHelper;
private IQuery<IContent> _queryNotTrashed;
#region Constructors
@@ -35,7 +37,7 @@ namespace Umbraco.Core.Services.Implement
IEventMessagesFactory eventMessagesFactory,
IDocumentRepository documentRepository, IEntityRepository entityRepository, IAuditRepository auditRepository,
IContentTypeRepository contentTypeRepository, IDocumentBlueprintRepository documentBlueprintRepository, ILanguageRepository languageRepository,
Lazy<IPropertyValidationService> propertyValidationService)
Lazy<IPropertyValidationService> propertyValidationService, IShortStringHelper shortStringHelper)
: base(provider, logger, eventMessagesFactory)
{
_documentRepository = documentRepository;
@@ -45,6 +47,7 @@ namespace Umbraco.Core.Services.Implement
_documentBlueprintRepository = documentBlueprintRepository;
_languageRepository = languageRepository;
_propertyValidationService = propertyValidationService;
_shortStringHelper = shortStringHelper;
}
#endregion
@@ -600,23 +603,27 @@ namespace Umbraco.Core.Services.Implement
totalChildren = 0;
return Enumerable.Empty<IContent>();
}
return GetPagedDescendantsLocked(contentPath[0].Path, pageIndex, pageSize, out totalChildren, filter, ordering);
return GetPagedLocked(GetPagedDescendantQuery(contentPath[0].Path), pageIndex, pageSize, out totalChildren, filter, ordering);
}
return GetPagedDescendantsLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering);
return GetPagedLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering);
}
}
private IEnumerable<IContent> GetPagedDescendantsLocked(string contentPath, long pageIndex, int pageSize, out long totalChildren,
private IQuery<IContent> GetPagedDescendantQuery(string contentPath)
{
var query = Query<IContent>();
if (!contentPath.IsNullOrWhiteSpace())
query.Where(x => x.Path.SqlStartsWith($"{contentPath},", TextColumnType.NVarchar));
return query;
}
private IEnumerable<IContent> GetPagedLocked(IQuery<IContent> query, long pageIndex, int pageSize, out long totalChildren,
IQuery<IContent> filter, Ordering ordering)
{
if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
if (ordering == null) throw new ArgumentNullException(nameof(ordering));
var query = Query<IContent>();
if (!contentPath.IsNullOrWhiteSpace())
query.Where(x => x.Path.SqlStartsWith($"{contentPath},", TextColumnType.NVarchar));
return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
}
@@ -1865,7 +1872,7 @@ namespace Umbraco.Core.Services.Implement
public OperationResult MoveToRecycleBin(IContent content, int userId)
{
var evtMsgs = EventMessagesFactory.Get();
var moves = new List<Tuple<IContent, string>>();
var moves = new List<(IContent, string)>();
using (var scope = ScopeProvider.CreateScope())
{
@@ -1924,7 +1931,7 @@ namespace Umbraco.Core.Services.Implement
return;
}
var moves = new List<Tuple<IContent, string>>();
var moves = new List<(IContent, string)>();
using (var scope = ScopeProvider.CreateScope())
{
@@ -1977,7 +1984,7 @@ namespace Umbraco.Core.Services.Implement
// MUST be called from within WriteLock
// trash indicates whether we are trashing, un-trashing, or not changing anything
private void PerformMoveLocked(IContent content, int parentId, IContent parent, int userId,
ICollection<Tuple<IContent, string>> moves,
ICollection<(IContent, string)> moves,
bool? trash)
{
content.WriterId = userId;
@@ -1989,7 +1996,7 @@ namespace Umbraco.Core.Services.Implement
var paths = new Dictionary<int, string>();
moves.Add(Tuple.Create(content, content.Path)); // capture original path
moves.Add((content, content.Path)); // capture original path
//need to store the original path to lookup descendants based on it below
var originalPath = content.Path;
@@ -2006,20 +2013,24 @@ namespace Umbraco.Core.Services.Implement
paths[content.Id] = (parent == null ? (parentId == Constants.System.RecycleBinContent ? "-1,-20" : Constants.System.RootString) : parent.Path) + "," + content.Id;
const int pageSize = 500;
var total = long.MaxValue;
while (total > 0)
var query = GetPagedDescendantQuery(originalPath);
long total;
do
{
var descendants = GetPagedDescendantsLocked(originalPath, 0, pageSize, out total, null, Ordering.By("Path", Direction.Ascending));
// We always page a page 0 because for each page, we are moving the result so the resulting total will be reduced
var descendants = GetPagedLocked(query, 0, pageSize, out total, null, Ordering.By("Path", Direction.Ascending));
foreach (var descendant in descendants)
{
moves.Add(Tuple.Create(descendant, descendant.Path)); // capture original path
moves.Add((descendant, descendant.Path)); // capture original path
// update path and level since we do not update parentId
descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id;
descendant.Level += levelDelta;
PerformMoveContentLocked(descendant, userId, trash);
}
}
} while (total > pageSize);
}
@@ -2367,6 +2378,25 @@ namespace Umbraco.Core.Services.Implement
return OperationResult.Succeed(evtMsgs);
}
public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
{
scope.WriteLock(Constants.Locks.ContentTree);
var report = _documentRepository.CheckDataIntegrity(options);
if (report.FixedIssues.Count > 0)
{
//The event args needs a content item so we'll make a fake one with enough properties to not cause a null ref
var root = new Content("root", -1, new ContentType(_shortStringHelper, -1)) {Id = -1, Key = Guid.Empty};
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>.EventArgs(new TreeChange<IContent>(root, TreeChangeTypes.RefreshAll)));
}
return report;
}
}
#endregion
#region Internal Methods
@@ -2804,7 +2834,7 @@ namespace Umbraco.Core.Services.Implement
// which we need for many things like keeping caches in sync, but we can surely do this MUCH better.
var changes = new List<TreeChange<IContent>>();
var moves = new List<Tuple<IContent, string>>();
var moves = new List<(IContent, string)>();
var contentTypeIdsA = contentTypeIds.ToArray();
// using an immediate uow here because we keep making changes with
@@ -13,6 +13,7 @@ using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services.Changes;
using Umbraco.Core.Strings;
namespace Umbraco.Core.Services.Implement
{
@@ -25,6 +26,7 @@ namespace Umbraco.Core.Services.Implement
private readonly IMediaTypeRepository _mediaTypeRepository;
private readonly IAuditRepository _auditRepository;
private readonly IEntityRepository _entityRepository;
private readonly IShortStringHelper _shortStringHelper;
private readonly IMediaFileSystem _mediaFileSystem;
@@ -32,7 +34,7 @@ namespace Umbraco.Core.Services.Implement
public MediaService(IScopeProvider provider, IMediaFileSystem mediaFileSystem, ILogger logger, IEventMessagesFactory eventMessagesFactory,
IMediaRepository mediaRepository, IAuditRepository auditRepository, IMediaTypeRepository mediaTypeRepository,
IEntityRepository entityRepository)
IEntityRepository entityRepository, IShortStringHelper shortStringHelper)
: base(provider, logger, eventMessagesFactory)
{
_mediaFileSystem = mediaFileSystem;
@@ -40,6 +42,7 @@ namespace Umbraco.Core.Services.Implement
_auditRepository = auditRepository;
_mediaTypeRepository = mediaTypeRepository;
_entityRepository = entityRepository;
_shortStringHelper = shortStringHelper;
}
#endregion
@@ -530,23 +533,27 @@ namespace Umbraco.Core.Services.Implement
totalChildren = 0;
return Enumerable.Empty<IMedia>();
}
return GetPagedDescendantsLocked(mediaPath[0].Path, pageIndex, pageSize, out totalChildren, filter, ordering);
return GetPagedLocked(GetPagedDescendantQuery(mediaPath[0].Path), pageIndex, pageSize, out totalChildren, filter, ordering);
}
return GetPagedDescendantsLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering);
return GetPagedLocked(GetPagedDescendantQuery(null), pageIndex, pageSize, out totalChildren, filter, ordering);
}
}
private IEnumerable<IMedia> GetPagedDescendantsLocked(string mediaPath, long pageIndex, int pageSize, out long totalChildren,
private IQuery<IMedia> GetPagedDescendantQuery(string mediaPath)
{
var query = Query<IMedia>();
if (!mediaPath.IsNullOrWhiteSpace())
query.Where(x => x.Path.SqlStartsWith(mediaPath + ",", TextColumnType.NVarchar));
return query;
}
private IEnumerable<IMedia> GetPagedLocked(IQuery<IMedia> query, long pageIndex, int pageSize, out long totalChildren,
IQuery<IMedia> filter, Ordering ordering)
{
if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
if (ordering == null) throw new ArgumentNullException(nameof(ordering));
var query = Query<IMedia>();
if (!mediaPath.IsNullOrWhiteSpace())
query.Where(x => x.Path.SqlStartsWith(mediaPath + ",", TextColumnType.NVarchar));
return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
}
@@ -888,7 +895,7 @@ namespace Umbraco.Core.Services.Implement
public Attempt<OperationResult> MoveToRecycleBin(IMedia media, int userId = Constants.Security.SuperUserId)
{
var evtMsgs = EventMessagesFactory.Get();
var moves = new List<Tuple<IMedia, string>>();
var moves = new List<(IMedia, string)>();
using (var scope = ScopeProvider.CreateScope())
{
@@ -940,7 +947,7 @@ namespace Umbraco.Core.Services.Implement
return OperationResult.Attempt.Succeed(evtMsgs);
}
var moves = new List<Tuple<IMedia, string>>();
var moves = new List<(IMedia, string)>();
using (var scope = ScopeProvider.CreateScope())
{
@@ -979,7 +986,7 @@ namespace Umbraco.Core.Services.Implement
// MUST be called from within WriteLock
// trash indicates whether we are trashing, un-trashing, or not changing anything
private void PerformMoveLocked(IMedia media, int parentId, IMedia parent, int userId, ICollection<Tuple<IMedia, string>> moves, bool? trash)
private void PerformMoveLocked(IMedia media, int parentId, IMedia parent, int userId, ICollection<(IMedia, string)> moves, bool? trash)
{
media.ParentId = parentId;
@@ -989,7 +996,7 @@ namespace Umbraco.Core.Services.Implement
var paths = new Dictionary<int, string>();
moves.Add(Tuple.Create(media, media.Path)); // capture original path
moves.Add((media, media.Path)); // capture original path
//need to store the original path to lookup descendants based on it below
var originalPath = media.Path;
@@ -1006,21 +1013,25 @@ namespace Umbraco.Core.Services.Implement
paths[media.Id] = (parent == null ? (parentId == Constants.System.RecycleBinMedia ? "-1,-21" : Constants.System.RootString) : parent.Path) + "," + media.Id;
const int pageSize = 500;
var page = 0;
var total = long.MaxValue;
while (page * pageSize < total)
var query = GetPagedDescendantQuery(originalPath);
long total;
do
{
var descendants = GetPagedDescendantsLocked(originalPath, page++, pageSize, out total, null, Ordering.By("Path", Direction.Ascending));
// We always page a page 0 because for each page, we are moving the result so the resulting total will be reduced
var descendants = GetPagedLocked(query, 0, pageSize, out total, null, Ordering.By("Path", Direction.Ascending));
foreach (var descendant in descendants)
{
moves.Add(Tuple.Create(descendant, descendant.Path)); // capture original path
moves.Add((descendant, descendant.Path)); // capture original path
// update path and level since we do not update parentId
descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id;
descendant.Level += levelDelta;
PerformMoveMediaLocked(descendant, userId, trash);
}
}
} while (total > pageSize);
}
private void PerformMoveMediaLocked(IMedia media, int userId, bool? trash)
@@ -1132,6 +1143,26 @@ namespace Umbraco.Core.Services.Implement
}
return true;
}
public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
{
scope.WriteLock(Constants.Locks.MediaTree);
var report = _mediaRepository.CheckDataIntegrity(options);
if (report.FixedIssues.Count > 0)
{
//The event args needs a content item so we'll make a fake one with enough properties to not cause a null ref
var root = new Models.Media("root", -1, new MediaType(_shortStringHelper, -1)) { Id = -1, Key = Guid.Empty };
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IMedia>.EventArgs(new TreeChange<IMedia>(root, TreeChangeTypes.RefreshAll)));
}
return report;
}
}
#endregion
@@ -1270,7 +1301,7 @@ namespace Umbraco.Core.Services.Implement
// which we need for many things like keeping caches in sync, but we can surely do this MUCH better.
var changes = new List<TreeChange<IMedia>>();
var moves = new List<Tuple<IMedia, string>>();
var moves = new List<(IMedia, string)>();
var mediaTypeIdsA = mediaTypeIds.ToArray();
using (var scope = ScopeProvider.CreateScope())
@@ -1351,5 +1382,7 @@ namespace Umbraco.Core.Services.Implement
}
#endregion
}
}
@@ -78,7 +78,10 @@
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Umbraco.Tests.Common</_Parameter1>
</AssemblyAttribute>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Umbraco.Tests.UnitTests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
@@ -505,6 +505,14 @@ namespace Umbraco.Web.PublishedCache.NuCache
}
}
/// <summary>
/// Validate the <see cref="ContentNodeKit"/> and try to create a parent <see cref="LinkedNode{ContentNode}"/>
/// </summary>
/// <param name="kit"></param>
/// <param name="parent"></param>
/// <returns>
/// Returns false if the parent was not found or if the kit validation failed
/// </returns>
private bool BuildKit(ContentNodeKit kit, out LinkedNode<ContentNode> parent)
{
// make sure parent exists
@@ -515,6 +523,15 @@ namespace Umbraco.Web.PublishedCache.NuCache
return false;
}
// We cannot continue if there's no value. This shouldn't happen but it can happen if the database umbracoNode.path
// data is invalid/corrupt. If that is the case, the parentId might be ok but not the Path which can result in null
// because the data sort operation is by path.
if (parent.Value == null)
{
_logger.Warn<ContentStore>($"Skip item id={kit.Node.Id}, no Data assigned for linked node with path {kit.Node.Path} and parent id {kit.Node.ParentContentId}. This can indicate data corruption for the Path value for node {kit.Node.Id}. See the Health Check dashboard in Settings to resolve data integrity issues.");
return false;
}
// make sure the kit is valid
if (kit.DraftData == null && kit.PublishedData == null)
{
@@ -803,7 +820,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
{
//this zero's out the branch (recursively), if we're in a new gen this will add a NULL placeholder for the gen
ClearBranchLocked(existing);
//TODO: This removes the current GEN from the tree - do we really want to do that?
//TODO: This removes the current GEN from the tree - do we really want to do that? (not sure if this is still an issue....)
RemoveTreeNodeLocked(existing);
}
@@ -868,6 +885,10 @@ namespace Umbraco.Web.PublishedCache.NuCache
private void ClearBranchLocked(ContentNode content)
{
// This should never be null, all code that calls this method is null checking but we've seen
// issues of null ref exceptions in issue reports so we'll double check here
if (content == null) throw new ArgumentNullException(nameof(content));
SetValueLocked(_contentNodes, content.Id, null);
if (_localDb != null) RegisterChange(content.Id, ContentNodeKit.Null);
@@ -876,9 +897,11 @@ namespace Umbraco.Web.PublishedCache.NuCache
var id = content.FirstChildContentId;
while (id > 0)
{
// get the required link node, this ensures that both `link` and `link.Value` are not null
var link = GetRequiredLinkedNode(id, "child", null);
ClearBranchLocked(link.Value);
id = link.Value.NextSiblingContentId;
var linkValue = link.Value; // capture local since clearing in recurse can clear it
ClearBranchLocked(linkValue); // recurse
id = linkValue.NextSiblingContentId;
}
}
@@ -1033,6 +1056,12 @@ namespace Umbraco.Web.PublishedCache.NuCache
var parent = parentLink.Value;
// We are doing a null check here but this should no longer be possible because we have a null check in BuildKit
// for the parent.Value property and we'll output a warning. However I'll leave this additional null check in place.
// see https://github.com/umbraco/Umbraco-CMS/issues/7868
if (parent == null)
throw new PanicException($"A null Value was returned on the {nameof(parentLink)} LinkedNode with id={content.ParentContentId}, potentially your database paths are corrupted.");
// if parent has no children, clone parent + add as first child
if (parent.FirstChildContentId < 0)
{
@@ -11,7 +11,7 @@
{
public LinkedNode(TValue value, long gen, LinkedNode<TValue> next = null)
{
Value = value;
Value = value; // This is allowed to be null, we actually explicitly set this to null in ClearLocked
Gen = gen;
Next = next;
}
@@ -7,12 +7,10 @@ namespace Umbraco.Tests.Common.Builders
{
private IDictionary<string, object> _defaultConfiguration;
public ConfigurationEditorBuilder(TParent parentBuilder) : base(parentBuilder)
{
}
public ConfigurationEditorBuilder<TParent> WithDefaultConfiguration(IDictionary<string, object> defaultConfiguration)
{
_defaultConfiguration = defaultConfiguration;
@@ -28,6 +26,5 @@ namespace Umbraco.Tests.Common.Builders
DefaultConfiguration = defaultConfiguration,
};
}
}
}
@@ -9,8 +9,8 @@ namespace Umbraco.Tests.Common.Builders
{
public class DataEditorBuilder<TParent> : ChildBuilderBase<TParent, IDataEditor>
{
private readonly ConfigurationEditorBuilder<DataEditorBuilder<TParent>> _explicitConfigurationEditorBuilder;
private readonly DataValueEditorBuilder<DataEditorBuilder<TParent>> _explicitValueEditorBuilder;
private ConfigurationEditorBuilder<DataEditorBuilder<TParent>> _explicitConfigurationEditorBuilder;
private DataValueEditorBuilder<DataEditorBuilder<TParent>> _explicitValueEditorBuilder;
private IDictionary<string, object> _defaultConfiguration;
public DataEditorBuilder(TParent parentBuilder) : base(parentBuilder)
@@ -19,7 +19,7 @@ namespace Umbraco.Tests.Common.Builders
IWithPathBuilder,
IWithSortOrderBuilder
{
private readonly DataEditorBuilder<DataTypeBuilder> _dataEditorBuilder;
private DataEditorBuilder<DataTypeBuilder> _dataEditorBuilder;
private int? _id;
private int? _parentId;
private Guid? _key;
@@ -28,7 +28,6 @@ namespace Umbraco.Tests.Common.Builders
private DateTime? _deleteDate;
private string _name;
private bool? _trashed;
// private object _configuration;
private int? _level;
private string _path;
private int? _creatorId;
@@ -40,12 +39,6 @@ namespace Umbraco.Tests.Common.Builders
_dataEditorBuilder = new DataEditorBuilder<DataTypeBuilder>(this);
}
// public DataTypeBuilder WithConfiguration(object configuration)
// {
// _configuration = configuration;
// return this;
// }
public DataTypeBuilder WithDatabaseType(ValueStorageType databaseType)
{
_databaseType = databaseType;
@@ -67,7 +60,6 @@ namespace Umbraco.Tests.Common.Builders
var updateDate = _updateDate ?? DateTime.Now;
var deleteDate = _deleteDate ?? null;
var name = _name ?? Guid.NewGuid().ToString();
// var configuration = _configuration ?? editor.GetConfigurationEditor().DefaultConfigurationObject;
var level = _level ?? 0;
var path = _path ?? string.Empty;
var creatorId = _creatorId ?? 1;
@@ -13,7 +13,6 @@ namespace Umbraco.Tests.Common.Builders
private bool? _hideLabel;
private string _valueType;
public DataValueEditorBuilder(TParent parentBuilder) : base(parentBuilder)
{
}
@@ -12,8 +12,7 @@ namespace Umbraco.Tests.Common.Builders
IWithDeleteDateBuilder,
IWithKeyBuilder
{
private readonly LanguageBuilder<DictionaryTranslationBuilder> _languageBuilder;
private readonly Guid? _uniqueId = null;
private LanguageBuilder<DictionaryTranslationBuilder> _languageBuilder;
private DateTime? _createDate;
private DateTime? _deleteDate;
private int? _id;
@@ -26,6 +25,36 @@ namespace Umbraco.Tests.Common.Builders
_languageBuilder = new LanguageBuilder<DictionaryTranslationBuilder>(this);
}
public LanguageBuilder<DictionaryTranslationBuilder> AddLanguage() => _languageBuilder;
public DictionaryTranslationBuilder WithValue(string value)
{
_value = value;
return this;
}
public override IDictionaryTranslation Build()
{
var createDate = _createDate ?? DateTime.Now;
var updateDate = _updateDate ?? DateTime.Now;
var deleteDate = _deleteDate ?? null;
var id = _id ?? 1;
var key = _key ?? Guid.NewGuid();
var result = new DictionaryTranslation(
_languageBuilder.Build(),
_value ?? Guid.NewGuid().ToString(),
key)
{
CreateDate = createDate,
UpdateDate = updateDate,
DeleteDate = deleteDate,
Id = id
};
return result;
}
DateTime? IWithCreateDateBuilder.CreateDate
{
get => _createDate;
@@ -55,35 +84,5 @@ namespace Umbraco.Tests.Common.Builders
get => _updateDate;
set => _updateDate = value;
}
public override IDictionaryTranslation Build()
{
var createDate = _createDate ?? DateTime.Now;
var updateDate = _updateDate ?? DateTime.Now;
var deleteDate = _deleteDate ?? null;
var id = _id ?? 1;
var key = _key ?? Guid.NewGuid();
var result = new DictionaryTranslation(
_languageBuilder.Build(),
_value ?? Guid.NewGuid().ToString(),
_uniqueId ?? key)
{
CreateDate = createDate,
UpdateDate = updateDate,
DeleteDate = deleteDate,
Id = id
};
return result;
}
public LanguageBuilder<DictionaryTranslationBuilder> AddLanguage() => _languageBuilder;
public DictionaryTranslationBuilder WithValue(string value)
{
_value = value;
return this;
}
}
}
@@ -0,0 +1,44 @@
using Umbraco.Core.Models.Entities;
using Umbraco.Tests.Common.Builders.Interfaces;
namespace Umbraco.Tests.Common.Builders
{
public class EntitySlimBuilder
: BuilderBase<EntitySlim>,
IWithIdBuilder,
IWithParentIdBuilder
{
private int? _id;
private int? _parentId;
public override EntitySlim Build()
{
var id = _id ?? 1;
var parentId = _parentId ?? -1;
return new EntitySlim
{
Id = id,
ParentId = parentId,
};
}
public EntitySlimBuilder WithNoParentId()
{
_parentId = 0;
return this;
}
int? IWithIdBuilder.Id
{
get => _id;
set => _id = value;
}
int? IWithParentIdBuilder.ParentId
{
get => _parentId;
set => _parentId = value;
}
}
}
@@ -12,6 +12,13 @@ namespace Umbraco.Tests.Common.Builders.Extensions
return builder;
}
public static T WithoutIdentity<T>(this T builder)
where T : IWithIdBuilder
{
builder.Id = 0;
return builder;
}
public static T WithCreatorId<T>(this T builder, int creatorId)
where T : IWithCreatorIdBuilder
{
@@ -116,5 +123,60 @@ namespace Umbraco.Tests.Common.Builders.Extensions
builder.Thumbnail = thumbnail;
return builder;
}
public static T WithLogin<T>(this T builder, string username, string rawPasswordValue)
where T : IWithLoginBuilder
{
builder.Username = username;
builder.RawPasswordValue = rawPasswordValue;
return builder;
}
public static T WithEmail<T>(this T builder, string email)
where T : IWithEmailBuilder
{
builder.Email = email;
return builder;
}
public static T WithFailedPasswordAttempts<T>(this T builder, int failedPasswordAttempts)
where T : IWithFailedPasswordAttemptsBuilder
{
builder.FailedPasswordAttempts = failedPasswordAttempts;
return builder;
}
public static T WithIsApproved<T>(this T builder, bool isApproved)
where T : IWithIsApprovedBuilder
{
builder.IsApproved = isApproved;
return builder;
}
public static T WithIsLockedOut<T>(this T builder, bool isLockedOut, DateTime? lastLockoutDate = null)
where T : IWithIsLockedOutBuilder
{
builder.IsLockedOut = isLockedOut;
if (lastLockoutDate.HasValue)
{
builder.LastLockoutDate = lastLockoutDate.Value;
}
return builder;
}
public static T WithLastLoginDate<T>(this T builder, DateTime lastLoginDate)
where T : IWithLastLoginDateBuilder
{
builder.LastLoginDate = lastLoginDate;
return builder;
}
public static T WithLastPasswordChangeDate<T>(this T builder, DateTime lastPasswordChangeDate)
where T : IWithLastPasswordChangeDateBuilder
{
builder.LastPasswordChangeDate = lastPasswordChangeDate;
return builder;
}
}
}
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
namespace Umbraco.Tests.Common.Builders
{
@@ -14,7 +15,8 @@ namespace Umbraco.Tests.Common.Builders
public override IEnumerable<T> Build()
{
return _collection;
var collection = _collection?.ToList() ?? Enumerable.Empty<T>();
return collection;
}
public GenericCollectionBuilder<TBuilder, T> WithValue(T value)
@@ -14,7 +14,9 @@ namespace Umbraco.Tests.Common.Builders
public override IDictionary<TKey, TValue> Build()
{
return _dictionary;
return _dictionary == null
? new Dictionary<TKey, TValue>()
: new Dictionary<TKey, TValue>(_dictionary);
}
public GenericDictionaryBuilder<TBuilder, TKey, TValue> WithKeyValue(TKey key, TValue value)
@@ -34,7 +34,6 @@ namespace Umbraco.Tests.Common.Builders
private int? _versionCheckPeriod;
private readonly SmtpSettingsBuilder<GlobalSettingsBuilder<TParent>> _smtpSettingsBuilder;
public GlobalSettingsBuilder(TParent parentBuilder) : base(parentBuilder)
{
_smtpSettingsBuilder = new SmtpSettingsBuilder<GlobalSettingsBuilder<TParent>>(this);
@@ -192,7 +191,6 @@ namespace Umbraco.Tests.Common.Builders
var mainDomLock = _mainDomLock ?? string.Empty;
var noNodesViewPath = _noNodesViewPath ?? "~/config/splashes/NoNodes.cshtml";
return new TestGlobalSettings
{
ConfigurationStatus = configurationStatus,
@@ -0,0 +1,12 @@
namespace Umbraco.Tests.Common.Builders.Interfaces
{
public interface IAccountBuilder : IWithLoginBuilder,
IWithEmailBuilder,
IWithFailedPasswordAttemptsBuilder,
IWithIsApprovedBuilder,
IWithIsLockedOutBuilder,
IWithLastLoginDateBuilder,
IWithLastPasswordChangeDateBuilder
{
}
}
@@ -1,7 +0,0 @@
namespace Umbraco.Tests.Common.Builders.Interfaces
{
public interface IWithApprovedBuilder
{
bool? Approved { get; set; }
}
}
@@ -0,0 +1,7 @@
namespace Umbraco.Tests.Common.Builders.Interfaces
{
public interface IWithEmailBuilder
{
string Email { get; set; }
}
}
@@ -0,0 +1,7 @@
namespace Umbraco.Tests.Common.Builders.Interfaces
{
public interface IWithFailedPasswordAttemptsBuilder
{
int? FailedPasswordAttempts { get; set; }
}
}
@@ -0,0 +1,7 @@
namespace Umbraco.Tests.Common.Builders.Interfaces
{
public interface IWithIsApprovedBuilder
{
bool? IsApproved { get; set; }
}
}
@@ -0,0 +1,11 @@
using System;
namespace Umbraco.Tests.Common.Builders.Interfaces
{
public interface IWithIsLockedOutBuilder
{
bool? IsLockedOut { get; set; }
DateTime? LastLockoutDate { get; set; }
}
}
@@ -0,0 +1,9 @@
using System;
namespace Umbraco.Tests.Common.Builders.Interfaces
{
public interface IWithLastLoginDateBuilder
{
DateTime? LastLoginDate { get; set; }
}
}
@@ -0,0 +1,9 @@
using System;
namespace Umbraco.Tests.Common.Builders.Interfaces
{
public interface IWithLastPasswordChangeDateBuilder
{
DateTime? LastPasswordChangeDate { get; set; }
}
}
@@ -0,0 +1,9 @@
namespace Umbraco.Tests.Common.Builders.Interfaces
{
public interface IWithLoginBuilder
{
string Username { get; set; }
string RawPasswordValue { get; set; }
}
}
@@ -37,6 +37,50 @@ namespace Umbraco.Tests.Common.Builders
{
}
public LanguageBuilder<TParent> WithIsDefault(bool isDefault)
{
_isDefault = isDefault;
return this;
}
public LanguageBuilder<TParent> WithIsMandatory(bool isMandatory)
{
_isMandatory = isMandatory;
return this;
}
public LanguageBuilder<TParent> WithFallbackLanguageId(int fallbackLanguageId)
{
_fallbackLanguageId = fallbackLanguageId;
return this;
}
public override ILanguage Build()
{
var cultureInfo = _cultureInfo ?? CultureInfo.GetCultureInfo("en-US");
var key = _key ?? Guid.NewGuid();
var createDate = _createDate ?? DateTime.Now;
var updateDate = _updateDate ?? DateTime.Now;
var deleteDate = _deleteDate ?? null;
var fallbackLanguageId = _fallbackLanguageId ?? null;
var isDefault = _isDefault ?? false;
var isMandatory = _isMandatory ?? false;
return new Language(Mock.Of<IGlobalSettings>(), cultureInfo.Name)
{
Id = _id ?? 1,
CultureName = cultureInfo.TwoLetterISOLanguageName,
IsoCode = new RegionInfo(cultureInfo.LCID).Name,
Key = key,
CreateDate = createDate,
UpdateDate = updateDate,
DeleteDate = deleteDate,
IsDefault = isDefault,
IsMandatory = isMandatory,
FallbackLanguageId = fallbackLanguageId
};
}
DateTime? IWithCreateDateBuilder.CreateDate
{
get => _createDate;
@@ -72,49 +116,5 @@ namespace Umbraco.Tests.Common.Builders
get => _updateDate;
set => _updateDate = value;
}
public override ILanguage Build()
{
var cultureInfo = _cultureInfo ?? CultureInfo.GetCultureInfo("en-US");
var key = _key ?? Guid.NewGuid();
var createDate = _createDate ?? DateTime.Now;
var updateDate = _updateDate ?? DateTime.Now;
var deleteDate = _deleteDate ?? null;
var fallbackLanguageId = _fallbackLanguageId ?? null;
var isDefault = _isDefault ?? false;
var isMandatory = _isMandatory ?? false;
return new Language(Mock.Of<IGlobalSettings>(), cultureInfo.Name)
{
Id = _id ?? 1,
CultureName = cultureInfo.TwoLetterISOLanguageName,
IsoCode = new RegionInfo(cultureInfo.LCID).Name,
Key = key,
CreateDate = createDate,
UpdateDate = updateDate,
DeleteDate = deleteDate,
IsDefault = isDefault,
IsMandatory = isMandatory,
FallbackLanguageId = fallbackLanguageId
};
}
public LanguageBuilder<TParent> WithIsDefault(bool isDefault)
{
_isDefault = isDefault;
return this;
}
public LanguageBuilder<TParent> WithIsMandatory(bool isMandatory)
{
_isMandatory = isMandatory;
return this;
}
public LanguageBuilder<TParent> WithFallbackLanguageId(int fallbackLanguageId)
{
_fallbackLanguageId = fallbackLanguageId;
return this;
}
}
}
@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core.Models;
using Umbraco.Core.Strings;
using Umbraco.Tests.Common.Builders.Extensions;
using Umbraco.Tests.Common.Builders.Interfaces;
namespace Umbraco.Tests.Common.Builders
{
public class MacroBuilder
: BuilderBase<Macro>,
IWithIdBuilder,
IWithKeyBuilder,
IWithAliasBuilder,
IWithNameBuilder
{
private List<MacroPropertyBuilder> _propertyBuilders = new List<MacroPropertyBuilder>();
private int? _id;
private Guid? _key;
private string _alias;
private string _name;
private bool? _useInEditor;
private int? _cacheDuration;
private bool? _cacheByPage;
private bool? _cacheByMember;
private bool? _dontRender;
private string _macroSource;
public MacroBuilder WithUseInEditor(bool useInEditor)
{
_useInEditor = useInEditor;
return this;
}
public MacroBuilder WithCacheDuration(int cacheDuration)
{
_cacheDuration = cacheDuration;
return this;
}
public MacroBuilder WithCacheByPage(bool cacheByPage)
{
_cacheByPage = cacheByPage;
return this;
}
public MacroBuilder WithCacheByMember(bool cacheByMember)
{
_cacheByMember = cacheByMember;
return this;
}
public MacroBuilder WithDontRender(bool dontRender)
{
_dontRender = dontRender;
return this;
}
public MacroBuilder WithSource(string macroSource)
{
_macroSource = macroSource;
return this;
}
public MacroPropertyBuilder AddProperty()
{
var builder = new MacroPropertyBuilder(this);
_propertyBuilders.Add(builder);
return builder;
}
public override Macro Build()
{
var id = _id ?? 1;
var name = _name ?? Guid.NewGuid().ToString();
var alias = _alias ?? name.ToCamelCase();
var key = _key ?? Guid.NewGuid();
var useInEditor = _useInEditor ?? false;
var cacheDuration = _cacheDuration ?? 0;
var cacheByPage = _cacheByPage ?? false;
var cacheByMember = _cacheByMember ?? false;
var dontRender = _dontRender ?? false;
var macroSource = _macroSource ?? string.Empty;
var shortStringHelper = new DefaultShortStringHelper(new DefaultShortStringHelperConfig());
var macro = new Macro(shortStringHelper, id, key, useInEditor, cacheDuration, alias, name, cacheByPage, cacheByMember, dontRender, macroSource);
foreach (var property in _propertyBuilders.Select(x => x.Build()))
{
macro.Properties.Add(property);
}
return macro;
}
int? IWithIdBuilder.Id
{
get => _id;
set => _id = value;
}
Guid? IWithKeyBuilder.Key
{
get => _key;
set => _key = value;
}
string IWithAliasBuilder.Alias
{
get => _alias;
set => _alias = value;
}
string IWithNameBuilder.Name
{
get => _name;
set => _name = value;
}
}
}
@@ -0,0 +1,75 @@
using System;
using Umbraco.Core.Models;
using Umbraco.Tests.Common.Builders.Extensions;
using Umbraco.Tests.Common.Builders.Interfaces;
namespace Umbraco.Tests.Common.Builders
{
public class MacroPropertyBuilder
: ChildBuilderBase<MacroBuilder, IMacroProperty>,
IWithIdBuilder,
IWithKeyBuilder,
IWithAliasBuilder,
IWithNameBuilder,
IWithSortOrderBuilder
{
private int? _id;
private Guid? _key;
private string _alias;
private string _name;
private int? _sortOrder;
private string _editorAlias;
public MacroPropertyBuilder(MacroBuilder parentBuilder) : base(parentBuilder)
{
}
public MacroPropertyBuilder WithEditorAlias(string editorAlias)
{
_editorAlias = editorAlias;
return this;
}
public override IMacroProperty Build()
{
var id = _id ?? 1;
var name = _name ?? Guid.NewGuid().ToString();
var alias = _alias ?? name.ToCamelCase();
var key = _key ?? Guid.NewGuid();
var sortOrder = _sortOrder ?? 0;
var editorAlias = _editorAlias ?? string.Empty;
return new MacroProperty(id, key, alias, name, sortOrder, editorAlias);
}
int? IWithIdBuilder.Id
{
get => _id;
set => _id = value;
}
Guid? IWithKeyBuilder.Key
{
get => _key;
set => _key = value;
}
string IWithAliasBuilder.Alias
{
get => _alias;
set => _alias = value;
}
string IWithNameBuilder.Name
{
get => _name;
set => _name = value;
}
int? IWithSortOrderBuilder.SortOrder
{
get => _sortOrder;
set => _sortOrder = value;
}
}
}
@@ -15,7 +15,8 @@ namespace Umbraco.Tests.Common.Builders
IWithTrashedBuilder,
IWithLevelBuilder,
IWithPathBuilder,
IWithSortOrderBuilder
IWithSortOrderBuilder,
IAccountBuilder
{
private MemberTypeBuilder _memberTypeBuilder;
private GenericCollectionBuilder<MemberBuilder, string> _memberGroupsBuilder;
@@ -28,12 +29,12 @@ namespace Umbraco.Tests.Common.Builders
private DateTime? _updateDate;
private string _name;
private int? _creatorId;
private int? _level;
private string _path;
private string _username;
private string _rawPasswordValue;
private string _email;
private int? _failedPasswordAttempts;
private int? _level;
private string _path;
private bool? _isApproved;
private bool? _isLockedOut;
private DateTime? _lastLockoutDate;
@@ -43,60 +44,6 @@ namespace Umbraco.Tests.Common.Builders
private bool? _trashed;
private int? _propertyIdsIncrementingFrom;
public MemberBuilder WithUserName(string username)
{
_username = username;
return this;
}
public MemberBuilder WithEmail(string email)
{
_email = email;
return this;
}
public MemberBuilder WithRawPasswordValue(string rawPasswordValue)
{
_rawPasswordValue = rawPasswordValue;
return this;
}
public MemberBuilder WithFailedPasswordAttempts(int failedPasswordAttempts)
{
_failedPasswordAttempts = failedPasswordAttempts;
return this;
}
public MemberBuilder WithIsApproved(bool isApproved)
{
_isApproved = isApproved;
return this;
}
public MemberBuilder WithIsLockedOut(bool isLockedOut)
{
_isLockedOut = isLockedOut;
return this;
}
public MemberBuilder WithLastLockoutDate(DateTime lastLockoutDate)
{
_lastLockoutDate = lastLockoutDate;
return this;
}
public MemberBuilder WithLastLoginDate(DateTime lastLoginDate)
{
_lastLoginDate = lastLoginDate;
return this;
}
public MemberBuilder WithLastPasswordChangeDate(DateTime lastPasswordChangeDate)
{
_lastPasswordChangeDate = lastPasswordChangeDate;
return this;
}
public MemberBuilder WithPropertyIdsIncrementingFrom(int propertyIdsIncrementingFrom)
{
_propertyIdsIncrementingFrom = propertyIdsIncrementingFrom;
@@ -139,19 +86,19 @@ namespace Umbraco.Tests.Common.Builders
var updateDate = _updateDate ?? DateTime.Now;
var name = _name ?? Guid.NewGuid().ToString();
var creatorId = _creatorId ?? 1;
var level = _level ?? 1;
var path = _path ?? "-1";
var sortOrder = _sortOrder ?? 0;
var trashed = _trashed ?? false;
var username = _username ?? string.Empty;
var email = _email ?? string.Empty;
var rawPasswordValue = _rawPasswordValue ?? string.Empty;
var failedPasswordAttempts = _failedPasswordAttempts ?? 0;
var level = _level ?? 1;
var path = _path ?? "-1";
var isApproved = _isApproved ?? false;
var isLockedOut = _isLockedOut ?? false;
var lastLockoutDate = _lastLockoutDate ?? DateTime.Now;
var lastLoginDate = _lastLoginDate ?? DateTime.Now;
var lastPasswordChangeDate = _lastPasswordChangeDate ?? DateTime.Now;
var sortOrder = _sortOrder ?? 0;
var trashed = _trashed ?? false;
if (_memberTypeBuilder == null)
{
@@ -276,5 +223,59 @@ namespace Umbraco.Tests.Common.Builders
get => _sortOrder;
set => _sortOrder = value;
}
string IWithLoginBuilder.Username
{
get => _username;
set => _username = value;
}
string IWithLoginBuilder.RawPasswordValue
{
get => _rawPasswordValue;
set => _rawPasswordValue = value;
}
string IWithEmailBuilder.Email
{
get => _email;
set => _email = value;
}
int? IWithFailedPasswordAttemptsBuilder.FailedPasswordAttempts
{
get => _failedPasswordAttempts;
set => _failedPasswordAttempts = value;
}
bool? IWithIsApprovedBuilder.IsApproved
{
get => _isApproved;
set => _isApproved = value;
}
bool? IWithIsLockedOutBuilder.IsLockedOut
{
get => _isLockedOut;
set => _isLockedOut = value;
}
DateTime? IWithIsLockedOutBuilder.LastLockoutDate
{
get => _lastLockoutDate;
set => _lastLockoutDate = value;
}
DateTime? IWithLastLoginDateBuilder.LastLoginDate
{
get => _lastLoginDate;
set => _lastLoginDate = value;
}
DateTime? IWithLastPasswordChangeDateBuilder.LastPasswordChangeDate
{
get => _lastPasswordChangeDate;
set => _lastPasswordChangeDate = value;
}
}
}
@@ -126,7 +126,7 @@ namespace Umbraco.Tests.Common.Builders
ValidationRegExpMessage = validationRegExpMessage,
};
}
int? IWithIdBuilder.Id
{
get => _id;
@@ -33,6 +33,47 @@ namespace Umbraco.Tests.Common.Builders
{
}
public RelationTypeBuilder WithIsBidirectional(bool isBidirectional)
{
_isBidirectional = isBidirectional;
return this;
}
public RelationTypeBuilder WithChildObjectType(Guid childObjectType)
{
_childObjectType = childObjectType;
return this;
}
public RelationTypeBuilder WithParentObjectType(Guid parentObjectType)
{
_parentObjectType = parentObjectType;
return this;
}
public override IRelationType Build()
{
var alias = _alias ?? Guid.NewGuid().ToString();
var name = _name ?? Guid.NewGuid().ToString();
var parentObjectType = _parentObjectType ?? null;
var childObjectType = _childObjectType ?? null;
var id = _id ?? 1;
var key = _key ?? Guid.NewGuid();
var isBidirectional = _isBidirectional ?? false;
var createDate = _createDate ?? DateTime.Now;
var updateDate = _updateDate ?? DateTime.Now;
var deleteDate = _deleteDate ?? null;
return new RelationType(name, alias, isBidirectional, parentObjectType, childObjectType)
{
Id = id,
Key = key,
CreateDate = createDate,
UpdateDate = updateDate,
DeleteDate = deleteDate
};
}
string IWithAliasBuilder.Alias
{
get => _alias;
@@ -74,46 +115,5 @@ namespace Umbraco.Tests.Common.Builders
get => _updateDate;
set => _updateDate = value;
}
public override IRelationType Build()
{
var alias = _alias ?? Guid.NewGuid().ToString();
var name = _name ?? Guid.NewGuid().ToString();
var parentObjectType = _parentObjectType ?? null;
var childObjectType = _childObjectType ?? null;
var id = _id ?? 1;
var key = _key ?? Guid.NewGuid();
var isBidirectional = _isBidirectional ?? false;
var createDate = _createDate ?? DateTime.Now;
var updateDate = _updateDate ?? DateTime.Now;
var deleteDate = _deleteDate ?? null;
return new RelationType(name, alias, isBidirectional, parentObjectType, childObjectType)
{
Id = id,
Key = key,
CreateDate = createDate,
UpdateDate = updateDate,
DeleteDate = deleteDate
};
}
public RelationTypeBuilder WithIsBidirectional(bool isBidirectional)
{
_isBidirectional = isBidirectional;
return this;
}
public RelationTypeBuilder WithChildObjectType(Guid childObjectType)
{
_childObjectType = childObjectType;
return this;
}
public RelationTypeBuilder WithParentObjectType(Guid parentObjectType)
{
_parentObjectType = parentObjectType;
return this;
}
}
}
@@ -9,65 +9,64 @@ namespace Umbraco.Tests.Common.Builders
}
}
public class SmtpSettingsBuilder<TParent>
: ChildBuilderBase<TParent, ISmtpSettings>
{
private string _from;
private string _host;
private int? _port;
private string _pickupDirectoryLocation;
public class SmtpSettingsBuilder<TParent>
: ChildBuilderBase<TParent, ISmtpSettings>
{
private string _from;
private string _host;
private int? _port;
private string _pickupDirectoryLocation;
public SmtpSettingsBuilder(TParent parentBuilder) : base(parentBuilder)
{
}
public SmtpSettingsBuilder(TParent parentBuilder) : base(parentBuilder)
{
}
public SmtpSettingsBuilder<TParent> WithFrom(string from)
{
_from = from;
return this;
}
public SmtpSettingsBuilder<TParent> WithFrom(string from)
{
_from = from;
return this;
}
public SmtpSettingsBuilder<TParent> WithHost(string host)
{
_host = host;
return this;
}
public SmtpSettingsBuilder<TParent> WithHost(string host)
{
_host = host;
return this;
}
public SmtpSettingsBuilder<TParent> WithPost(int port)
{
_port = port;
return this;
}
public SmtpSettingsBuilder<TParent> WithPost(int port)
{
_port = port;
return this;
}
public SmtpSettingsBuilder<TParent> WithPickupDirectoryLocation(string pickupDirectoryLocation)
{
_pickupDirectoryLocation = pickupDirectoryLocation;
return this;
}
public SmtpSettingsBuilder<TParent> WithPickupDirectoryLocation(string pickupDirectoryLocation)
{
_pickupDirectoryLocation = pickupDirectoryLocation;
return this;
}
public override ISmtpSettings Build()
{
var from = _from ?? null;
var host = _host ?? null;
var port = _port ?? 25;
var pickupDirectoryLocation = _pickupDirectoryLocation ?? null;
public override ISmtpSettings Build()
{
var from = _from ?? null;
var host = _host ?? null;
var port = _port ?? 25;
var pickupDirectoryLocation = _pickupDirectoryLocation ?? null;
return new TestSmtpSettings()
{
return new TestSmtpSettings()
{
From = from,
Host = host,
Port = port,
PickupDirectoryLocation = pickupDirectoryLocation,
};
}
};
}
private class TestSmtpSettings : ISmtpSettings
{
public string From { get; set; }
public string Host { get; set; }
public int Port { get; set; }
public string PickupDirectoryLocation { get; set; }
}
}
private class TestSmtpSettings : ISmtpSettings
{
public string From { get; set; }
public string Host { get; set; }
public int Port { get; set; }
public string PickupDirectoryLocation { get; set; }
}
}
}
@@ -57,6 +57,7 @@ namespace Umbraco.Tests.Common.Builders
var masterTemplateId = _masterTemplateId ?? null;
var shortStringHelper = new DefaultShortStringHelper(new DefaultShortStringHelperConfig());
return new Template(shortStringHelper, name, alias)
{
Id = id,
+130 -29
View File
@@ -1,41 +1,69 @@
using Umbraco.Configuration.Models;
using System;
using Umbraco.Core.Models.Membership;
using Umbraco.Tests.Common.Builders.Interfaces;
namespace Umbraco.Tests.Common.Builders
{
public class UserBuilder : UserBuilder<object>
{
public UserBuilder() : base(null)
{
}
}
public class UserBuilder<TParent>
: ChildBuilderBase<TParent, User>,
IWithIdBuilder,
IWithKeyBuilder,
IWithCreateDateBuilder,
IWithUpdateDateBuilder,
IWithNameBuilder,
IWithApprovedBuilder
IAccountBuilder
{
private int? _id;
private Guid? _key;
private DateTime? _createDate;
private DateTime? _updateDate;
private string _language;
private bool? _approved;
private string _name;
private string _rawPassword;
private bool? _isLockedOut;
private string _email;
private string _username;
private string _rawPasswordValue;
private string _email;
private int? _failedPasswordAttempts;
private bool? _isApproved;
private bool? _isLockedOut;
private DateTime? _lastLockoutDate;
private DateTime? _lastLoginDate;
private DateTime? _lastPasswordChangeDate;
private string _suffix = string.Empty;
private string _defaultLang;
private string _comments;
private int? _sessionTimeout;
private int[] _startContentIds;
private int[] _startMediaIds;
public UserBuilder(TParent parentBuilder) : base(parentBuilder)
{
}
public UserBuilder<TParent> WithDefaultUILanguage(string defaultLang)
Guid? IWithKeyBuilder.Key
{
get => _key;
set => _key = value;
}
DateTime? IWithCreateDateBuilder.CreateDate
{
get => _createDate;
set => _createDate = value;
}
DateTime? IWithUpdateDateBuilder.UpdateDate
{
get => _updateDate;
set => _updateDate = value;
}
public UserBuilder<TParent> WithDefaultUILanguage(string defaultLang)
{
_defaultLang = defaultLang;
return this;
@@ -47,27 +75,27 @@ namespace Umbraco.Tests.Common.Builders
return this;
}
public UserBuilder<TParent> WithRawPassword(string rawPassword)
public UserBuilder<TParent> WithComments(string comments)
{
_rawPassword = rawPassword;
_comments = comments;
return this;
}
public UserBuilder<TParent> WithEmail(string email)
public UserBuilder<TParent> WithSessionTimeout(int sessionTimeout)
{
_email = email;
_sessionTimeout = sessionTimeout;
return this;
}
public UserBuilder<TParent> WithUsername(string username)
public UserBuilder<TParent> WithStartContentIds(int[] startContentIds)
{
_username = username;
_startContentIds = startContentIds;
return this;
}
public UserBuilder<TParent> WithLockedOut(bool isLockedOut)
public UserBuilder<TParent> WithStartMediaIds(int[] startMediaIds)
{
_isLockedOut = isLockedOut;
_startMediaIds = startMediaIds;
return this;
}
@@ -84,25 +112,50 @@ namespace Umbraco.Tests.Common.Builders
public override User Build()
{
var globalSettings = new GlobalSettingsBuilder().WithDefaultUiLanguage(_defaultLang).Build();
var id = _id ?? 1;
var defaultLang = _defaultLang ?? "en";
var globalSettings = new GlobalSettingsBuilder().WithDefaultUiLanguage(defaultLang).Build();
var key = _key ?? Guid.NewGuid();
var createDate = _createDate ?? DateTime.Now;
var updateDate = _updateDate ?? DateTime.Now;
var name = _name ?? "TestUser" + _suffix;
var email = _email ?? "test" + _suffix + "@test.com";
var username = _username ?? "TestUser" + _suffix;
var rawPassword = _rawPassword ?? "abcdefghijklmnopqrstuvwxyz";
var language = _language ?? globalSettings.DefaultUILanguage;
var username = _username ?? "TestUser" + _suffix;
var email = _email ?? "test" + _suffix + "@test.com";
var rawPasswordValue = _rawPasswordValue ?? "abcdefghijklmnopqrstuvwxyz";
var failedPasswordAttempts = _failedPasswordAttempts ?? 0;
var isApproved = _isApproved ?? false;
var isLockedOut = _isLockedOut ?? false;
var approved = _approved ?? true;
var lastLockoutDate = _lastLockoutDate ?? DateTime.Now;
var lastLoginDate = _lastLoginDate ?? DateTime.Now;
var lastPasswordChangeDate = _lastPasswordChangeDate ?? DateTime.Now;
var comments = _comments ?? string.Empty;
var sessionTimeout = _sessionTimeout ?? 0;
var startContentIds = _startContentIds ?? new int[0];
var startMediaIds = _startMediaIds ?? new int[0];
return new User(
globalSettings,
name,
email,
username,
rawPassword)
rawPasswordValue)
{
Id = id,
Key = key,
CreateDate = createDate,
UpdateDate = updateDate,
Language = language,
FailedPasswordAttempts = failedPasswordAttempts,
IsApproved = isApproved,
IsLockedOut = isLockedOut,
IsApproved = approved
LastLockoutDate = lastLockoutDate,
LastLoginDate = lastLoginDate,
LastPasswordChangeDate = lastPasswordChangeDate,
Comments = comments,
SessionTimeout = sessionTimeout,
StartContentIds = startContentIds,
StartMediaIds = startMediaIds,
};
}
@@ -118,10 +171,58 @@ namespace Umbraco.Tests.Common.Builders
set => _name = value;
}
bool? IWithApprovedBuilder.Approved
string IWithLoginBuilder.Username
{
get => _approved;
set => _approved = value;
get => _username;
set => _username = value;
}
string IWithLoginBuilder.RawPasswordValue
{
get => _rawPasswordValue;
set => _rawPasswordValue = value;
}
string IWithEmailBuilder.Email
{
get => _email;
set => _email = value;
}
int? IWithFailedPasswordAttemptsBuilder.FailedPasswordAttempts
{
get => _failedPasswordAttempts;
set => _failedPasswordAttempts = value;
}
bool? IWithIsApprovedBuilder.IsApproved
{
get => _isApproved;
set => _isApproved = value;
}
bool? IWithIsLockedOutBuilder.IsLockedOut
{
get => _isLockedOut;
set => _isLockedOut = value;
}
DateTime? IWithIsLockedOutBuilder.LastLockoutDate
{
get => _lastLockoutDate;
set => _lastLockoutDate = value;
}
DateTime? IWithLastLoginDateBuilder.LastLoginDate
{
get => _lastLoginDate;
set => _lastLoginDate = value;
}
DateTime? IWithLastPasswordChangeDateBuilder.LastPasswordChangeDate
{
get => _lastPasswordChangeDate;
set => _lastPasswordChangeDate = value;
}
}
}
@@ -6,7 +6,6 @@ using Umbraco.Tests.Common.Builders.Interfaces;
namespace Umbraco.Tests.Common.Builders
{
public class UserGroupBuilder : UserGroupBuilder<object>
{
public UserGroupBuilder() : base(null)
@@ -61,7 +60,7 @@ namespace Umbraco.Tests.Common.Builders
public override IUserGroup Build()
{
return Mock.Of<IUserGroup>(x =>
var userGroup = Mock.Of<IUserGroup>(x =>
x.StartContentId == _startContentId &&
x.StartMediaId == _startMediaId &&
x.Name == (_name ?? ("TestUserGroup" + _suffix)) &&
@@ -69,9 +68,10 @@ namespace Umbraco.Tests.Common.Builders
x.Icon == _icon &&
x.Permissions == _permissions &&
x.AllowedSections == _sectionCollection);
return userGroup;
}
int? IWithIdBuilder.Id
int? IWithIdBuilder.Id
{
get => _id;
set => _id = value;
@@ -152,5 +152,14 @@ namespace Umbraco.Tests.Common
return mock.Object;
}
public ILoggingConfiguration GetLoggingConfiguration(IHostingEnvironment hostingEnv = null)
{
hostingEnv = hostingEnv ?? GetHostingEnvironment();
return new LoggingConfiguration(
Path.Combine(hostingEnv.ApplicationPhysicalPath, "App_Data\\Logs"),
Path.Combine(hostingEnv.ApplicationPhysicalPath, "config\\serilog.config"),
Path.Combine(hostingEnv.ApplicationPhysicalPath, "config\\serilog.user.config"));
}
}
}
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Hosting;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
using Umbraco.Web.Common.AspNetCore;
using IHostingEnvironment = Umbraco.Core.Hosting.IHostingEnvironment;
@@ -1,21 +1,19 @@
using System.Linq;
using System;
using System.Linq;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Logging;
using Umbraco.Core.Configuration;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.Mappers;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.Persistence.Repositories.Implement;
using Umbraco.Core.Scoping;
using Umbraco.Tests.Testing;
using Umbraco.Core.Persistence;
using Umbraco.Core.PropertyEditors;
using System;
using Umbraco.Core.Configuration;
using Umbraco.Core.Services.Implement;
using Umbraco.Tests.Common.Builders.Extensions;
using Umbraco.Tests.Integration.Testing;
using Umbraco.Tests.Testing;
namespace Umbraco.Tests.Persistence.Repositories
{
@@ -89,7 +87,7 @@ namespace Umbraco.Tests.Persistence.Repositories
{
var repository = CreateRepository(provider);
var user = UserBuilder.Build();
var user = UserBuilder.WithoutIdentity().Build();
repository.Save(user);
@@ -367,10 +365,9 @@ namespace Umbraco.Tests.Persistence.Repositories
private User CreateAndCommitUserWithGroup(IUserRepository repository, IUserGroupRepository userGroupRepository)
{
var user = UserBuilder.Build();
var user = UserBuilder.WithoutIdentity().Build();
repository.Save(user);
var group = UserGroupBuilder.Build();
userGroupRepository.AddOrUpdateGroupWithUsers(@group, new[] { user.Id });
@@ -381,9 +378,9 @@ namespace Umbraco.Tests.Persistence.Repositories
private IUser[] CreateAndCommitMultipleUsers(IUserRepository repository)
{
var user1 = UserBuilder.WithSuffix("1").Build();
var user2 = UserBuilder.WithSuffix("2").Build();
var user3 = UserBuilder.WithSuffix("3").Build();
var user1 = UserBuilder.WithoutIdentity().WithSuffix("1").Build();
var user2 = UserBuilder.WithoutIdentity().WithSuffix("2").Build();
var user3 = UserBuilder.WithoutIdentity().WithSuffix("3").Build();
repository.Save(user1);
repository.Save(user2);
repository.Save(user3);
@@ -4,8 +4,10 @@ using Moq;
using NUnit.Framework;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Smidge;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Composing;
using Umbraco.Core.Logging;
using Umbraco.Core.Runtime;
@@ -57,7 +59,7 @@ namespace Umbraco.Tests.Integration
var coreRuntime = new CoreRuntime(testHelper.GetConfigs(), testHelper.GetUmbracoVersion(),
testHelper.IOHelper, testHelper.Logger, testHelper.Profiler, testHelper.UmbracoBootPermissionChecker,
testHelper.GetHostingEnvironment(), testHelper.GetBackOfficeInfo(), testHelper.DbProviderFactoryCreator,
testHelper.MainDom, testHelper.GetTypeFinder());
testHelper.MainDom, testHelper.GetTypeFinder(), NoAppCache.Instance);
// boot it!
var factory = coreRuntime.Configure(umbracoContainer);
@@ -99,7 +101,7 @@ namespace Umbraco.Tests.Integration
// Add it!
services.AddUmbracoConfiguration(hostContext.Configuration);
services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, out _);
services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, NoAppCache.Instance, testHelper.GetLoggingConfiguration(), out _);
});
var host = await hostBuilder.StartAsync();
@@ -138,7 +140,7 @@ namespace Umbraco.Tests.Integration
// Add it!
services.AddUmbracoConfiguration(hostContext.Configuration);
services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, out _);
services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, NoAppCache.Instance, testHelper.GetLoggingConfiguration(), out _);
});
var host = await hostBuilder.StartAsync();
@@ -1,11 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NPoco.Expressions;
using NUnit.Framework;
using Umbraco.Core.Cache;
using Umbraco.Core.Composing;
@@ -21,7 +18,6 @@ using Umbraco.Tests.Common.Builders;
using Umbraco.Tests.Integration.Extensions;
using Umbraco.Tests.Integration.Implementations;
using Umbraco.Web.BackOffice.AspNetCore;
using Umbraco.Web.Common.AspNetCore;
using Umbraco.Web.Common.Extensions;
namespace Umbraco.Tests.Integration.Testing
@@ -112,7 +108,7 @@ namespace Umbraco.Tests.Integration.Testing
// Add it!
services.AddUmbracoConfiguration(hostContext.Configuration);
services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, out _);
services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, NoAppCache.Instance, testHelper.GetLoggingConfiguration(), out _);
});
var host = await hostBuilder.StartAsync();
@@ -9,7 +9,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models
[TestFixture]
public class DataTypeTests
{
private readonly DataTypeBuilder _builder = new DataTypeBuilder();
private DataTypeBuilder _builder;
[SetUp]
public void SetUp()
{
_builder = new DataTypeBuilder();
}
[Test]
public void Can_Deep_Clone()
@@ -9,7 +9,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models
[TestFixture]
public class DictionaryItemTests
{
private readonly DictionaryItemBuilder _builder = new DictionaryItemBuilder();
private DictionaryItemBuilder _builder = new DictionaryItemBuilder();
[SetUp]
public void SetUp()
{
_builder = new DictionaryItemBuilder();
}
[Test]
public void Can_Deep_Clone()
@@ -8,7 +8,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models
[TestFixture]
public class LanguageTests
{
private readonly LanguageBuilder _builder = new LanguageBuilder();
private LanguageBuilder _builder = new LanguageBuilder();
[SetUp]
public void SetUp()
{
_builder = new LanguageBuilder();
}
[Test]
public void Can_Deep_Clone()
@@ -3,18 +3,42 @@ using System.Linq;
using NUnit.Framework;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Entities;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.Common.Builders;
using Umbraco.Tests.Common.Builders.Extensions;
namespace Umbraco.Tests.Models
namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models
{
[TestFixture]
public class MacroTests
{
private MacroBuilder _builder;
[SetUp]
public void SetUp()
{
_builder = new MacroBuilder();
}
[Test]
public void Can_Deep_Clone()
{
var macro = new Macro(TestHelper.ShortStringHelper, 1, Guid.NewGuid(), true, 3, "test", "Test", false, true, true, "~/script.cshtml");
macro.Properties.Add(new MacroProperty(6, Guid.NewGuid(), "rewq", "REWQ", 1, "asdfasdf"));
var macro = _builder
.WithId(1)
.WithUseInEditor(true)
.WithCacheDuration(3)
.WithAlias("test")
.WithName("Test")
.WithSource("~/script.cshtml")
.WithCacheByMember(true)
.WithDontRender(true)
.AddProperty()
.WithId(6)
.WithAlias("rewq")
.WithName("REWQ")
.WithSortOrder(1)
.WithEditorAlias("asdfasdf")
.Done()
.Build();
var clone = (Macro)macro.DeepClone();
@@ -24,7 +48,7 @@ namespace Umbraco.Tests.Models
Assert.AreEqual(clone.Properties.Count, macro.Properties.Count);
for (int i = 0; i < clone.Properties.Count; i++)
for (var i = 0; i < clone.Properties.Count; i++)
{
Assert.AreEqual(clone.Properties[i], macro.Properties[i]);
Assert.AreNotSame(clone.Properties[i], macro.Properties[i]);
@@ -37,9 +61,7 @@ namespace Umbraco.Tests.Models
//This double verifies by reflection
var allProps = clone.GetType().GetProperties();
foreach (var propertyInfo in allProps)
{
Assert.AreEqual(propertyInfo.GetValue(clone, null), propertyInfo.GetValue(macro, null));
}
//need to ensure the event handlers are wired
@@ -51,8 +73,6 @@ namespace Umbraco.Tests.Models
Assert.AreEqual(1, clone.AddedProperties.Count());
clone.Properties.Remove("rewq");
Assert.AreEqual(1, clone.RemovedProperties.Count());
}
}
}

Some files were not shown because too many files have changed in this diff Show More