diff --git a/.github/README.md b/.github/README.md
index 467ca6e5e6..d4565a8cb5 100644
--- a/.github/README.md
+++ b/.github/README.md
@@ -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).
-
diff --git a/.gitignore b/.gitignore
index 5f2432313f..d8c3f27d5a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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/*
diff --git a/src/Umbraco.Core/Cache/GenericDictionaryRequestAppCache.cs b/src/Umbraco.Core/Cache/GenericDictionaryRequestAppCache.cs
new file mode 100644
index 0000000000..193235ca7e
--- /dev/null
+++ b/src/Umbraco.Core/Cache/GenericDictionaryRequestAppCache.cs
@@ -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
+{
+ ///
+ /// Implements a fast on top of HttpContext.Items.
+ ///
+ ///
+ /// 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.
+ ///
+ public class GenericDictionaryRequestAppCache : FastDictionaryAppCacheBase, IRequestCache
+ {
+ ///
+ /// Initializes a new instance of the class with a context, for unit tests!
+ ///
+ public GenericDictionaryRequestAppCache(Func> requestItems) : base()
+ {
+ ContextItems = requestItems;
+ }
+
+ private Func> ContextItems { get; }
+
+ public bool IsAvailable => TryGetContextItems(out _);
+
+ private bool TryGetContextItems(out IDictionary items)
+ {
+ items = ContextItems?.Invoke();
+ return items != null;
+ }
+
+ ///
+ public override object Get(string key, Func factory)
+ {
+ //no place to cache so just return the callback result
+ if (!TryGetContextItems(out var items)) return factory();
+
+ key = GetCacheKey(key);
+
+ Lazy result;
+
+ try
+ {
+ EnterWriteLock();
+ result = items[key] as Lazy; // 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 GetDictionaryEntries()
+ {
+ const string prefix = CacheItemPrefix + "-";
+
+ if (!TryGetContextItems(out var items)) return Enumerable.Empty();
+
+ return items.Cast()
+ .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> GetEnumerator()
+ {
+ if (!TryGetContextItems(out var items))
+ {
+ yield break;
+ }
+
+ foreach (var item in items)
+ {
+ yield return new KeyValuePair(item.Key.ToString(), item.Value);
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ }
+}
diff --git a/src/Umbraco.Core/Constants-SqlTemplates.cs b/src/Umbraco.Core/Constants-SqlTemplates.cs
new file mode 100644
index 0000000000..984bc495b0
--- /dev/null
+++ b/src/Umbraco.Core/Constants-SqlTemplates.cs
@@ -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";
+ }
+
+ }
+ }
+}
diff --git a/src/Umbraco.Core/ContentExtensions.cs b/src/Umbraco.Core/ContentExtensions.cs
index b5d5915137..3e70bcda53 100644
--- a/src/Umbraco.Core/ContentExtensions.cs
+++ b/src/Umbraco.Core/ContentExtensions.cs
@@ -6,6 +6,7 @@ namespace Umbraco.Core
{
public static class ContentExtensions
{
+
#region XML methods
///
diff --git a/src/Umbraco.Core/Hosting/IHostingEnvironment.cs b/src/Umbraco.Core/Hosting/IHostingEnvironment.cs
index 0bdfe5c425..b653f535fa 100644
--- a/src/Umbraco.Core/Hosting/IHostingEnvironment.cs
+++ b/src/Umbraco.Core/Hosting/IHostingEnvironment.cs
@@ -6,6 +6,10 @@ namespace Umbraco.Core.Hosting
{
string SiteName { get; }
string ApplicationId { get; }
+
+ ///
+ /// Will return the physical path to the root of the application
+ ///
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 ?
+
+ ///
+ /// Maps a virtual path to a physical path to the application's web root
+ ///
+ ///
+ ///
+ ///
+ /// 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.
+ ///
string MapPath(string path);
///
- /// 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
///
/// The virtual path. Must start with either ~/ or / else an exception is thrown.
///
diff --git a/src/Umbraco.Core/Logging/ILoggingConfiguration.cs b/src/Umbraco.Core/Logging/ILoggingConfiguration.cs
new file mode 100644
index 0000000000..47e2d8fa7c
--- /dev/null
+++ b/src/Umbraco.Core/Logging/ILoggingConfiguration.cs
@@ -0,0 +1,13 @@
+namespace Umbraco.Core.Logging
+{
+
+ public interface ILoggingConfiguration
+ {
+ ///
+ /// The physical path where logs are stored
+ ///
+ string LogDirectory { get; }
+ string LogConfigurationFile { get; }
+ string UserLogConfigurationFile { get; }
+ }
+}
diff --git a/src/Umbraco.Core/Logging/IProfiler.cs b/src/Umbraco.Core/Logging/IProfiler.cs
index 1327651197..d855612c95 100644
--- a/src/Umbraco.Core/Logging/IProfiler.cs
+++ b/src/Umbraco.Core/Logging/IProfiler.cs
@@ -2,18 +2,12 @@
namespace Umbraco.Core.Logging
{
+
///
/// Defines the profiling service.
///
public interface IProfiler
{
- ///
- /// Renders the profiling results.
- ///
- /// The profiling results.
- /// Generally used for HTML rendering.
- string Render();
-
///
/// Gets an that will time the code between its creation and disposal.
///
diff --git a/src/Umbraco.Core/Logging/IProfilerHtml.cs b/src/Umbraco.Core/Logging/IProfilerHtml.cs
new file mode 100644
index 0000000000..4f9ee62e0b
--- /dev/null
+++ b/src/Umbraco.Core/Logging/IProfilerHtml.cs
@@ -0,0 +1,15 @@
+namespace Umbraco.Core.Logging
+{
+ ///
+ /// Used to render a profiler in a web page
+ ///
+ public interface IProfilerHtml
+ {
+ ///
+ /// Renders the profiling results.
+ ///
+ /// The profiling results.
+ /// Generally used for HTML rendering.
+ string Render();
+ }
+}
diff --git a/src/Umbraco.Core/Logging/LogProfiler.cs b/src/Umbraco.Core/Logging/LogProfiler.cs
index 294f92dad3..a1d2a2e61f 100644
--- a/src/Umbraco.Core/Logging/LogProfiler.cs
+++ b/src/Umbraco.Core/Logging/LogProfiler.cs
@@ -15,12 +15,6 @@ namespace Umbraco.Core.Logging
_logger = logger;
}
- ///
- public string Render()
- {
- return string.Empty;
- }
-
///
public IDisposable Step(string name)
{
diff --git a/src/Umbraco.Core/Logging/LoggingConfiguration.cs b/src/Umbraco.Core/Logging/LoggingConfiguration.cs
new file mode 100644
index 0000000000..c657c9d430
--- /dev/null
+++ b/src/Umbraco.Core/Logging/LoggingConfiguration.cs
@@ -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; }
+ }
+}
diff --git a/src/Umbraco.Core/Logging/VoidProfiler.cs b/src/Umbraco.Core/Logging/VoidProfiler.cs
index 51bec521a3..d771fd7630 100644
--- a/src/Umbraco.Core/Logging/VoidProfiler.cs
+++ b/src/Umbraco.Core/Logging/VoidProfiler.cs
@@ -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;
diff --git a/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs b/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs
new file mode 100644
index 0000000000..9f0f147083
--- /dev/null
+++ b/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs
@@ -0,0 +1,48 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Umbraco.Core.Models
+{
+ public class ContentDataIntegrityReport
+ {
+ public ContentDataIntegrityReport(IReadOnlyDictionary detectedIssues)
+ {
+ DetectedIssues = detectedIssues;
+ }
+
+ public bool Ok => DetectedIssues.Count == 0 || DetectedIssues.Count == DetectedIssues.Values.Count(x => x.Fixed);
+
+ public IReadOnlyDictionary DetectedIssues { get; }
+
+ public IReadOnlyDictionary FixedIssues
+ => DetectedIssues.Where(x => x.Value.Fixed).ToDictionary(x => x.Key, x => x.Value);
+
+ public enum IssueType
+ {
+ ///
+ /// The item's level and path are inconsistent with it's parent's path and level
+ ///
+ InvalidPathAndLevelByParentId,
+
+ ///
+ /// The item's path doesn't contain all required parts
+ ///
+ InvalidPathEmpty,
+
+ ///
+ /// The item's path parts are inconsistent with it's level value
+ ///
+ InvalidPathLevelMismatch,
+
+ ///
+ /// The item's path does not end with it's own ID
+ ///
+ InvalidPathById,
+
+ ///
+ /// The item's path does not have it's parent Id as the 2nd last entry
+ ///
+ InvalidPathByParentId,
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Models/ContentDataIntegrityReportEntry.cs b/src/Umbraco.Core/Models/ContentDataIntegrityReportEntry.cs
new file mode 100644
index 0000000000..517b9e80dc
--- /dev/null
+++ b/src/Umbraco.Core/Models/ContentDataIntegrityReportEntry.cs
@@ -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; }
+ }
+}
diff --git a/src/Umbraco.Core/Models/ContentDataIntegrityReportOptions.cs b/src/Umbraco.Core/Models/ContentDataIntegrityReportOptions.cs
new file mode 100644
index 0000000000..c4689467c1
--- /dev/null
+++ b/src/Umbraco.Core/Models/ContentDataIntegrityReportOptions.cs
@@ -0,0 +1,13 @@
+namespace Umbraco.Core.Models
+{
+ public class ContentDataIntegrityReportOptions
+ {
+ ///
+ /// Set to true to try to automatically resolve data integrity issues
+ ///
+ 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...
+ }
+}
diff --git a/src/Umbraco.Core/Net/NullSessionIdResolver.cs b/src/Umbraco.Core/Net/NullSessionIdResolver.cs
new file mode 100644
index 0000000000..6bfa578268
--- /dev/null
+++ b/src/Umbraco.Core/Net/NullSessionIdResolver.cs
@@ -0,0 +1,7 @@
+namespace Umbraco.Net
+{
+ public class NullSessionIdResolver : ISessionIdResolver
+ {
+ public string SessionId => null;
+ }
+}
diff --git a/src/Umbraco.Core/Services/IContentServiceBase.cs b/src/Umbraco.Core/Services/IContentServiceBase.cs
index 439c55d0d0..c40f49347f 100644
--- a/src/Umbraco.Core/Services/IContentServiceBase.cs
+++ b/src/Umbraco.Core/Services/IContentServiceBase.cs
@@ -1,9 +1,16 @@
-namespace Umbraco.Core.Services
+using Umbraco.Core.Models;
+
+namespace Umbraco.Core.Services
{
///
/// Placeholder for sharing logic between the content, media (and member) services
/// TODO: Start sharing the logic!
///
public interface IContentServiceBase : IService
- { }
+ {
+ ///
+ /// Checks/fixes the data integrity of node paths/levels stored in the database
+ ///
+ ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options);
+ }
}
diff --git a/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs b/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs
index 2099778185..ab758d42af 100644
--- a/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs
+++ b/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs
@@ -13,8 +13,17 @@ namespace Umbraco.Core.Composing
///
///
public static IHostBuilder UseUmbraco(this IHostBuilder builder)
- => builder.UseUmbraco(new UmbracoServiceProviderFactory());
+ {
+ return builder
+ .UseUmbraco(new UmbracoServiceProviderFactory());
+ }
+ ///
+ /// Assigns a custom service provider factory to use Umbraco's container
+ ///
+ ///
+ ///
+ ///
public static IHostBuilder UseUmbraco(this IHostBuilder builder, UmbracoServiceProviderFactory umbracoServiceProviderFactory)
=> builder.UseServiceProviderFactory(umbracoServiceProviderFactory);
}
diff --git a/src/Umbraco.Infrastructure/ContentExtensions.cs b/src/Umbraco.Infrastructure/ContentExtensions.cs
index 158e365958..d8d39cc984 100644
--- a/src/Umbraco.Infrastructure/ContentExtensions.cs
+++ b/src/Umbraco.Infrastructure/ContentExtensions.cs
@@ -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;
+ }
+
///
/// Removes characters that are not valid XML characters from all entity properties
/// of type string. See: http://stackoverflow.com/a/961504/5018
diff --git a/src/Umbraco.Infrastructure/Diagnostics/MiniDump.cs b/src/Umbraco.Infrastructure/Diagnostics/MiniDump.cs
index 9bc0b1c3fb..57e9b5204b 100644
--- a/src/Umbraco.Infrastructure/Diagnostics/MiniDump.cs
+++ b/src/Umbraco.Infrastructure/Diagnostics/MiniDump.cs
@@ -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;
diff --git a/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs b/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs
index 4640007e1a..712ff85e16 100644
--- a/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs
+++ b/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs
@@ -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 MinimalLogger = new Lazy(() => 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);
diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs
index 45468ace9f..704e80d302 100644
--- a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs
+++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs
@@ -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
///
- internal class HttpRequestIdEnricher : ILogEventEnricher
+ public class HttpRequestIdEnricher : ILogEventEnricher
{
- private readonly Func _requestCacheGetter;
+ private readonly IRequestCache _requestCache;
- public HttpRequestIdEnricher(Func requestCacheGetter)
+ public HttpRequestIdEnricher(IRequestCache requestCache)
{
- _requestCacheGetter = requestCacheGetter;
+ _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache));
}
///
@@ -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));
diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs
index 08eb6b93f0..20643ff539 100644
--- a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs
+++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs
@@ -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
///
- internal class HttpRequestNumberEnricher : ILogEventEnricher
+ public class HttpRequestNumberEnricher : ILogEventEnricher
{
- private readonly Func _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 requestCacheGetter)
+ public HttpRequestNumberEnricher(IRequestCache requestCache)
{
- _requestCacheGetter = requestCacheGetter;
+ _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache));
}
///
@@ -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));
diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs
index 1558cdcf21..19572b5b42 100644
--- a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs
+++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs
@@ -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
///
- internal class HttpSessionIdEnricher : ILogEventEnricher
+ public class HttpSessionIdEnricher : ILogEventEnricher
{
private readonly ISessionIdResolver _sessionIdResolver;
diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/ThreadAbortExceptionEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/ThreadAbortExceptionEnricher.cs
new file mode 100644
index 0000000000..1f495d3a50
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/ThreadAbortExceptionEnricher.cs
@@ -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
+{
+ ///
+ /// Enriches the log if there are ThreadAbort exceptions and will automatically create a minidump if it can
+ ///
+ 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");
+ }
+
+
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs
index f4e8f85281..dfcc401ea3 100644
--- a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs
+++ b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs
@@ -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
///
/// A Serilog LoggerConfiguration
- ///
- public static LoggerConfiguration MinimalConfiguration(this LoggerConfiguration logConfig, IHostingEnvironment hostingEnvironment, ISessionIdResolver sessionIdResolver, Func requestCacheGetter)
+ ///
+ ///
+ 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()
- .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/
///
/// A Serilog LoggerConfiguration
+ ///
/// The log level you wish the JSON file to collect - default is Verbose (highest)
/// The number of days to keep log files. Default is set to null which means all logs are kept
- public static LoggerConfiguration 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/
///
/// A Serilog LoggerConfiguration
+ ///
/// The log level you wish the JSON file to collect - default is Verbose (highest)
/// The number of days to keep log files. Default is set to null which means all logs are kept
- public static LoggerConfiguration OutputDefaultJsonFile(this LoggerConfiguration logConfig, 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
///
/// A Serilog LoggerConfiguration
- public static LoggerConfiguration ReadFromConfigFile(this LoggerConfiguration logConfig)
+ ///
+ 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
///
/// A Serilog LoggerConfiguration
- public static LoggerConfiguration ReadFromUserConfigFile(this LoggerConfiguration logConfig)
+ ///
+ 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;
}
+
}
}
diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogComposer.cs b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogComposer.cs
new file mode 100644
index 0000000000..18b417d428
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogComposer.cs
@@ -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();
+ composition.RegisterUnique();
+ composition.RegisterUnique();
+ composition.RegisterUnique();
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs
index bb77869e28..38af9554ab 100644
--- a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs
+++ b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs
@@ -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
{
+
///
/// Implements on top of Serilog.
///
public class SerilogLogger : ILogger, IDisposable
{
- private readonly ICoreDebugSettings _coreDebugSettings;
- private readonly IIOHelper _ioHelper;
- private readonly IMarchal _marchal;
+ public global::Serilog.ILogger SerilogLog { get; }
///
/// Initialize a new instance of the class with a configuration file.
///
///
- 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();
}
///
/// Creates a logger with some pre-defined configuration and remainder from config file
///
/// Used by UmbracoApplicationBase to get its logger.
- public static SerilogLogger CreateWithDefaultConfiguration(IHostingEnvironment hostingEnvironment, ISessionIdResolver sessionIdResolver, Func 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);
}
///
/// Gets a contextualized logger.
///
private global::Serilog.ILogger LoggerFor(Type reporting)
- => Log.Logger.ForContext(reporting);
+ => SerilogLog.ForContext(reporting);
///
/// Maps Umbraco's log level to Serilog's.
@@ -99,8 +83,7 @@ namespace Umbraco.Core.Logging.Serilog
///
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
///
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);
}
///
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);
- }
-
///
public void Warn(Type reporting, string message)
{
@@ -289,7 +210,7 @@ namespace Umbraco.Core.Logging.Serilog
public void Dispose()
{
- Log.CloseAndFlush();
+ SerilogLog.DisposeIfDisposable();
}
}
}
diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs
index dbdd7842ba..6763b0ebbb 100644
--- a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs
+++ b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs
@@ -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
{
diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs
new file mode 100644
index 0000000000..14f35361e6
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+
+namespace Umbraco.Core.Logging.Viewer
+{
+ public interface ILogViewerConfig
+ {
+ IReadOnlyList GetSavedSearches();
+ IReadOnlyList AddSavedSearch(string name, string query);
+ IReadOnlyList DeleteSavedSearch(string name, string query);
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerComposer.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerComposer.cs
index 79680a3d53..ee115be325 100644
--- a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerComposer.cs
+++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerComposer.cs
@@ -9,7 +9,8 @@ namespace Umbraco.Core.Logging.Viewer
{
public void Compose(Composition composition)
{
- composition.SetLogViewer(factory => new JsonLogViewer(composition.Logger, factory.GetInstance()));
+ composition.RegisterUnique();
+ composition.SetLogViewer();
}
}
}
diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs
new file mode 100644
index 0000000000..5511cd87c7
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs
@@ -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 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(rawJson);
+ }
+
+ public IReadOnlyList 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 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("[]");
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/JsonLogViewer.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs
similarity index 87%
rename from src/Umbraco.Infrastructure/Logging/Viewer/JsonLogViewer.cs
rename to src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs
index aea1c8fae4..366a0fb9de 100644
--- a/src/Umbraco.Infrastructure/Logging/Viewer/JsonLogViewer.cs
+++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs
@@ -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();
- //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(ex, "Unable to parse a line in the JSON log file");
+ _logger.Error(ex, "Unable to parse a line in the JSON log file");
evt = null;
return true;
diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerSourceBase.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs
similarity index 60%
rename from src/Umbraco.Infrastructure/Logging/Viewer/LogViewerSourceBase.cs
rename to src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs
index 4cc70eaf42..7c8503a37e 100644
--- a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerSourceBase.cs
+++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs
@@ -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 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(rawJson);
- }
+ => _logViewerConfig.GetSavedSearches();
public virtual IReadOnlyList 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 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
///
public string GetLogLevel()
{
- var logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast().Where(Log.Logger.IsEnabled)?.Min() ?? null;
+ var logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast().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);
- }
- }
+
}
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/IContentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/IContentRepository.cs
index 217719e144..ad9e2d27c1 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/IContentRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/IContentRepository.cs
@@ -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
/// Here, can be null but cannot.
IEnumerable GetPage(IQuery query, long pageIndex, int pageSize, out long totalRecords,
IQuery filter, Ordering ordering);
+
+ ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options);
}
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs
index 90f8d454ac..6c216e938f 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs
@@ -91,7 +91,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
// gets all version ids, current first
public virtual IEnumerable 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(x => x.Id)
.From()
.Where(x => x.NodeId == SqlTemplate.Arg("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().From().Where(x => x.Id == SqlTemplate.Arg("versionId"))
);
var versionDto = Database.Fetch(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().From().Where(x => x.NodeId == SqlTemplate.Arg("nodeId") && !x.Current && x.VersionDate < SqlTemplate.Arg("versionDate"))
);
var versionDtos = Database.Fetch(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("ctype").On((content, contentType) => content.ContentTypeId == contentType.NodeId, aliasRight: "ctype");
@@ -485,6 +485,123 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
IQuery filter,
Ordering ordering);
+ public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options)
+ {
+ var report = new Dictionary();
+
+ var sql = SqlContext.Sql()
+ .Select()
+ .From()
+ .Where(x => x.NodeObjectType == NodeObjectTypeId)
+ .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder);
+
+ var nodesToRebuild = new Dictionary>();
+ var validNodes = new Dictionary();
+ var rootIds = new[] {Constants.System.Root, Constants.System.RecycleBinContent, Constants.System.RecycleBinMedia};
+ var currentParentIds = new HashSet(rootIds);
+ var prevParentIds = currentParentIds;
+ var lastLevel = -1;
+
+ // use a forward cursor (query)
+ foreach (var node in Database.Query(sql))
+ {
+ if (node.Level != lastLevel)
+ {
+ // changing levels
+ prevParentIds = currentParentIds;
+ currentParentIds = null;
+ lastLevel = node.Level;
+ }
+
+ if (currentParentIds == null)
+ {
+ // we're reset
+ currentParentIds = new HashSet();
+ }
+
+ 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();
+
+ 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> nodesToRebuild, NodeDto node)
+ {
+ if (nodesToRebuild.TryGetValue(node.ParentId, out var childIds))
+ childIds.Add(node);
+ else
+ nodesToRebuild[node.ParentId] = new List { node };
+ }
+
// here, filter can be null and ordering cannot
protected IEnumerable GetPage(IQuery query,
long pageIndex, int pageSize, out long totalRecords,
@@ -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(x => Alias(x.NodeId, "id"), x => Alias(x.Text, "name"))
.From()
.Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && x.ParentId == SqlTemplate.Arg("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().Where(x => x.ParentId == SqlTemplate.Arg("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().From().Where(x => x.NodeId == SqlTemplate.Arg("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(x => x.NodeId).From().Where(x => x.UniqueId == SqlTemplate.Arg("uniqueId") && x.NodeObjectType == Constants.ObjectTypes.IdReservation)
);
var id = Database.ExecuteScalar(template.Sql(new { uniqueId = uniqueId }));
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs
index ff4ac4e4dd..e02844f562 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs
@@ -331,7 +331,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
.InnerJoin()
.On((c, d) => c.Id == d.Id)
.Where(x => x.NodeId == SqlTemplate.Arg("nodeId") && !x.Current && x.VersionDate < SqlTemplate.Arg("versionDate"))
- .Where( x => !x.Published)
+ .Where(x => !x.Published)
);
var versionDtos = Database.Fetch(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(u => u.Set(x => x.Published, false)).Where(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(u => u.Set(x => x.Published, false)).Where(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().Where(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(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().Where(x => x.VersionId == versionToDelete);
- Database.Execute(deleteContentVariations);
+ var versionToDelete = publishing ? entity.PublishedVersionId : entity.VersionId;
+ var deletePropertyDataSql = Sql().Delete().Where(x => x.VersionId == versionToDelete);
+ Database.Execute(deletePropertyDataSql);
- // replace the document version variations (rather than updating)
- var deleteDocumentVariations = Sql().Delete().Where(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(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().Where(x => x.VersionId == versionToDelete);
+ Database.Execute(deleteContentVariations);
+
+ // replace the document version variations (rather than updating)
+ var deleteDocumentVariations = Sql().Delete().Where(x => x.NodeId == entity.Id);
+ Database.Execute(deleteDocumentVariations);
+
+ // TODO: NPoco InsertBulk issue?
+ // we should use the native NPoco InsertBulk here but it causes problems (not sure exactly all scenarios)
+ // but by using SQL Server and updating a variants name will cause: Unable to cast object of type
+ // 'Umbraco.Core.Persistence.FaultHandling.RetryDbConnection' to type 'System.Data.SqlClient.SqlConnection'.
+ // (same in PersistNewItem above)
+
+ // insert content variations
+ Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing));
+
+ // insert document variations
+ Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures));
+ }
+
+ // 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();
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs
index 11f8c4d696..83088de9bd 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs
@@ -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().Where(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().Where(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));
diff --git a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs
index 1ea08e3118..19d3716e1c 100644
--- a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs
+++ b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs
@@ -29,6 +29,7 @@ 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;
@@ -351,10 +352,6 @@ namespace Umbraco.Core.Runtime
// register accessors for cultures
composition.RegisterUnique();
-
-
-
-
}
}
}
diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs
index 92e47771a6..ebd91f52a2 100644
--- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs
+++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs
@@ -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("Booting Core");
+
+ Logger.Info("Booting site '{HostingSiteName}', app '{HostingApplicationId}', path '{HostingPhysicalPath}', server '{MachineName}'.",
+ HostingEnvironment?.SiteName,
+ HostingEnvironment?.ApplicationId,
+ HostingEnvironment?.ApplicationPhysicalPath,
+ NetworkHelper.MachineName);
Logger.Debug("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();
_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())));
}
diff --git a/src/Umbraco.Infrastructure/Runtime/WebRuntime.cs b/src/Umbraco.Infrastructure/Runtime/WebRuntime.cs
deleted file mode 100644
index fc2a019023..0000000000
--- a/src/Umbraco.Infrastructure/Runtime/WebRuntime.cs
+++ /dev/null
@@ -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
-{
- ///
- /// Represents the Web Umbraco runtime.
- ///
- /// On top of CoreRuntime, handles all of the web-related runtime aspects of Umbraco.
- public class WebRuntime : CoreRuntime
- {
- private readonly IRequestCache _requestCache;
-
- ///
- /// Initializes a new instance of the class.
- ///
- 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;
- }
-
- ///
- public override IFactory Configure(IRegister register)
- {
-
- var profilingLogger = new ProfilingLogger(Logger, Profiler);
- var umbracoVersion = new UmbracoVersion();
- using (var timer = profilingLogger.TraceDuration(
- $"Booting Umbraco {umbracoVersion.SemanticVersion.ToSemanticString()}.",
- "Booted.",
- "Boot failed."))
- {
- Logger.Info("Booting site '{HostingSiteName}', app '{HostingApplicationId}', path '{HostingPhysicalPath}', server '{MachineName}'.",
- HostingEnvironment.SiteName,
- HostingEnvironment.ApplicationId,
- HostingEnvironment.ApplicationPhysicalPath,
- NetworkHelper.MachineName);
- Logger.Debug("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
- }
-}
-
diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs b/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs
index d02f84d294..ec28614905 100644
--- a/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs
+++ b/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs
@@ -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 _propertyValidationService;
+ private readonly IShortStringHelper _shortStringHelper;
private IQuery _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 propertyValidationService)
+ Lazy 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();
}
- 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 GetPagedDescendantsLocked(string contentPath, long pageIndex, int pageSize, out long totalChildren,
+ private IQuery GetPagedDescendantQuery(string contentPath)
+ {
+ var query = Query();
+ if (!contentPath.IsNullOrWhiteSpace())
+ query.Where(x => x.Path.SqlStartsWith($"{contentPath},", TextColumnType.NVarchar));
+ return query;
+ }
+
+ private IEnumerable GetPagedLocked(IQuery query, long pageIndex, int pageSize, out long totalChildren,
IQuery 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();
- 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>();
+ 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>();
+ 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> moves,
+ ICollection<(IContent, string)> moves,
bool? trash)
{
content.WriterId = userId;
@@ -1989,7 +1996,7 @@ namespace Umbraco.Core.Services.Implement
var paths = new Dictionary();
- 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.EventArgs(new TreeChange(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>();
- var moves = new List>();
+ var moves = new List<(IContent, string)>();
var contentTypeIdsA = contentTypeIds.ToArray();
// using an immediate uow here because we keep making changes with
diff --git a/src/Umbraco.Infrastructure/Services/Implement/MediaService.cs b/src/Umbraco.Infrastructure/Services/Implement/MediaService.cs
index 14bebc4eb8..5e9854ad9e 100644
--- a/src/Umbraco.Infrastructure/Services/Implement/MediaService.cs
+++ b/src/Umbraco.Infrastructure/Services/Implement/MediaService.cs
@@ -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();
}
- 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 GetPagedDescendantsLocked(string mediaPath, long pageIndex, int pageSize, out long totalChildren,
+ private IQuery GetPagedDescendantQuery(string mediaPath)
+ {
+ var query = Query();
+ if (!mediaPath.IsNullOrWhiteSpace())
+ query.Where(x => x.Path.SqlStartsWith(mediaPath + ",", TextColumnType.NVarchar));
+ return query;
+ }
+
+ private IEnumerable GetPagedLocked(IQuery query, long pageIndex, int pageSize, out long totalChildren,
IQuery 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();
- 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 MoveToRecycleBin(IMedia media, int userId = Constants.Security.SuperUserId)
{
var evtMsgs = EventMessagesFactory.Get();
- var moves = new List>();
+ 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>();
+ 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> 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();
- 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.EventArgs(new TreeChange(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>();
- var moves = new List>();
+ 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
+
+
}
}
diff --git a/src/Umbraco.PublishedCache.NuCache/ContentStore.cs b/src/Umbraco.PublishedCache.NuCache/ContentStore.cs
index 9611ecb653..ae3f9ae472 100644
--- a/src/Umbraco.PublishedCache.NuCache/ContentStore.cs
+++ b/src/Umbraco.PublishedCache.NuCache/ContentStore.cs
@@ -505,6 +505,14 @@ namespace Umbraco.Web.PublishedCache.NuCache
}
}
+ ///
+ /// Validate the and try to create a parent
+ ///
+ ///
+ ///
+ ///
+ /// Returns false if the parent was not found or if the kit validation failed
+ ///
private bool BuildKit(ContentNodeKit kit, out LinkedNode 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($"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);
@@ -1035,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)
{
diff --git a/src/Umbraco.PublishedCache.NuCache/Snap/LinkedNode.cs b/src/Umbraco.PublishedCache.NuCache/Snap/LinkedNode.cs
index d187996df8..94f83ac4e5 100644
--- a/src/Umbraco.PublishedCache.NuCache/Snap/LinkedNode.cs
+++ b/src/Umbraco.PublishedCache.NuCache/Snap/LinkedNode.cs
@@ -11,7 +11,7 @@
{
public LinkedNode(TValue value, long gen, LinkedNode 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;
}
diff --git a/src/Umbraco.Tests.Common/TestHelperBase.cs b/src/Umbraco.Tests.Common/TestHelperBase.cs
index 42b1e6c0dd..21b1f66395 100644
--- a/src/Umbraco.Tests.Common/TestHelperBase.cs
+++ b/src/Umbraco.Tests.Common/TestHelperBase.cs
@@ -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"));
+ }
}
}
diff --git a/src/Umbraco.Tests.Integration/Implementations/TestHostingEnvironment.cs b/src/Umbraco.Tests.Integration/Implementations/TestHostingEnvironment.cs
index 6430291bc2..9f29b14858 100644
--- a/src/Umbraco.Tests.Integration/Implementations/TestHostingEnvironment.cs
+++ b/src/Umbraco.Tests.Integration/Implementations/TestHostingEnvironment.cs
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Umbraco.Core.Configuration;
+using Umbraco.Core.Logging;
using Umbraco.Web.Common.AspNetCore;
namespace Umbraco.Tests.Integration.Implementations
diff --git a/src/Umbraco.Tests.Integration/RuntimeTests.cs b/src/Umbraco.Tests.Integration/RuntimeTests.cs
index 52c29d2037..ad040c8ef1 100644
--- a/src/Umbraco.Tests.Integration/RuntimeTests.cs
+++ b/src/Umbraco.Tests.Integration/RuntimeTests.cs
@@ -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();
diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs
index 101feb79a4..0137850408 100644
--- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs
+++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs
@@ -108,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();
diff --git a/src/Umbraco.Tests/Logging/LogviewerTests.cs b/src/Umbraco.Tests/Logging/LogviewerTests.cs
index 87cc19a2c6..0a193b4446 100644
--- a/src/Umbraco.Tests/Logging/LogviewerTests.cs
+++ b/src/Umbraco.Tests/Logging/LogviewerTests.cs
@@ -1,9 +1,11 @@
using Moq;
using NUnit.Framework;
+using Serilog;
using System;
using System.IO;
using System.Linq;
using Umbraco.Core;
+using Umbraco.Core.Logging;
using Umbraco.Core.Logging.Viewer;
using Umbraco.Tests.TestHelpers;
@@ -33,13 +35,16 @@ namespace Umbraco.Tests.Logging
//Create an example JSON log file to check results
//As a one time setup for all tets in this class/fixture
var ioHelper = TestHelper.IOHelper;
+ var hostingEnv = TestHelper.GetHostingEnvironment();
+
+ var loggingConfiguration = TestHelper.GetLoggingConfiguration(hostingEnv);
var exampleLogfilePath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"Logging\", _logfileName);
- _newLogfileDirPath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"App_Data\Logs\");
+ _newLogfileDirPath = loggingConfiguration.LogDirectory;
_newLogfilePath = Path.Combine(_newLogfileDirPath, _logfileName);
var exampleSearchfilePath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"Logging\", _searchfileName);
- _newSearchfileDirPath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"Config\");
+ _newSearchfileDirPath = Path.Combine(hostingEnv.ApplicationPhysicalPath, @"Config\");
_newSearchfilePath = Path.Combine(_newSearchfileDirPath, _searchfileName);
//Create/ensure Directory exists
@@ -51,7 +56,8 @@ namespace Umbraco.Tests.Logging
File.Copy(exampleSearchfilePath, _newSearchfilePath, true);
var logger = Mock.Of();
- _logViewer = new JsonLogViewer(logger, ioHelper, logsPath: _newLogfileDirPath, searchPath: _newSearchfilePath);
+ var logViewerConfig = new LogViewerConfig(hostingEnv);
+ _logViewer = new SerilogJsonLogViewer(logger, logViewerConfig, loggingConfiguration, Log.Logger);
}
[OneTimeTearDown]
diff --git a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs
index b900453a5e..960d355c0d 100644
--- a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs
+++ b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs
@@ -16,10 +16,10 @@ using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi;
using Umbraco.Core.Strings;
using Umbraco.Core.Configuration;
-using Umbraco.Core.Dictionary;
using Umbraco.Core.Hosting;
using Umbraco.Core.IO;
using Umbraco.Core.Models.PublishedContent;
+using Umbraco.Core.Runtime;
using Umbraco.Core.Services;
using Umbraco.Tests.PublishedContent;
using Umbraco.Tests.Testing;
@@ -47,10 +47,10 @@ namespace Umbraco.Tests.Routing
HostingEnvironment);
}
- public class TestRuntime : WebRuntime
+ public class TestRuntime : CoreRuntime
{
public TestRuntime(Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, ILogger logger, IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo)
- : base(configs, umbracoVersion, ioHelper, Mock.Of(), Mock.Of(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, TestHelper.GetTypeFinder(), TestHelper.GetRequestCache(), new AspNetUmbracoBootPermissionChecker())
+ : base(configs, umbracoVersion, ioHelper, Mock.Of(), Mock.Of(), new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, TestHelper.GetTypeFinder(), NoAppCache.Instance)
{
}
diff --git a/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs b/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs
index 488a4f6dad..f63c56b64e 100644
--- a/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs
+++ b/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs
@@ -5,6 +5,7 @@ using Examine;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
+using Umbraco.Core.Cache;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.UmbracoSettings;
@@ -16,6 +17,7 @@ using Umbraco.Core.Logging;
using Umbraco.Core.Persistence;
using Umbraco.Core.Runtime;
using Umbraco.Core.Scoping;
+using Umbraco.Net;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.TestHelpers.Stubs;
using Umbraco.Web;
@@ -116,7 +118,7 @@ namespace Umbraco.Tests.Runtimes
public class TestRuntime : CoreRuntime
{
public TestRuntime(Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, ILogger logger, IProfiler profiler, IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo)
- :base(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, TestHelper.GetTypeFinder())
+ :base(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, TestHelper.GetTypeFinder(), NoAppCache.Instance)
{
}
@@ -134,6 +136,7 @@ namespace Umbraco.Tests.Runtimes
public override IFactory Configure(IRegister container)
{
container.Register(Lifetime.Singleton);
+ container.Register(Lifetime.Singleton);
var factory = base.Configure(container);
return factory;
diff --git a/src/Umbraco.Tests/Runtimes/StandaloneTests.cs b/src/Umbraco.Tests/Runtimes/StandaloneTests.cs
index 1a4c7f2040..5100e2e21c 100644
--- a/src/Umbraco.Tests/Runtimes/StandaloneTests.cs
+++ b/src/Umbraco.Tests/Runtimes/StandaloneTests.cs
@@ -76,7 +76,7 @@ namespace Umbraco.Tests.Runtimes
var runtimeState = new RuntimeState(logger, null, umbracoVersion, backOfficeInfo);
var configs = TestHelper.GetConfigs();
var variationContextAccessor = TestHelper.VariationContextAccessor;
-
+
// create the register and the composition
var register = TestHelper.GetRegister();
@@ -84,7 +84,7 @@ namespace Umbraco.Tests.Runtimes
composition.RegisterEssentials(logger, profiler, profilingLogger, mainDom, appCaches, databaseFactory, typeLoader, runtimeState, typeFinder, ioHelper, umbracoVersion, TestHelper.DbProviderFactoryCreator, hostingEnvironment, backOfficeInfo);
// create the core runtime and have it compose itself
- var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, typeFinder);
+ var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, typeFinder, NoAppCache.Instance);
// determine actual runtime level
runtimeState.DetermineRuntimeLevel(databaseFactory, logger);
@@ -278,7 +278,7 @@ namespace Umbraco.Tests.Runtimes
composition.RegisterEssentials(logger, profiler, profilingLogger, mainDom, appCaches, databaseFactory, typeLoader, runtimeState, typeFinder, ioHelper, umbracoVersion, TestHelper.DbProviderFactoryCreator, hostingEnvironment, backOfficeInfo);
// create the core runtime and have it compose itself
- var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, typeFinder);
+ var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, typeFinder, NoAppCache.Instance);
// get the components
// all of them?
@@ -322,8 +322,8 @@ namespace Umbraco.Tests.Runtimes
Assert.AreEqual(0, results.Count);
}
-
-
+
+
}
}
diff --git a/src/Umbraco.Tests/TestHelpers/Stubs/TestProfiler.cs b/src/Umbraco.Tests/TestHelpers/Stubs/TestProfiler.cs
index 39cac6e24f..ea0f9cc44f 100644
--- a/src/Umbraco.Tests/TestHelpers/Stubs/TestProfiler.cs
+++ b/src/Umbraco.Tests/TestHelpers/Stubs/TestProfiler.cs
@@ -19,11 +19,6 @@ namespace Umbraco.Tests.TestHelpers.Stubs
private static bool _enabled;
- public string Render()
- {
- return string.Empty;
- }
-
public IDisposable Step(string name)
{
return _enabled ? MiniProfiler.Current.Step(name) : null;
diff --git a/src/Umbraco.Tests/TestHelpers/TestHelper.cs b/src/Umbraco.Tests/TestHelpers/TestHelper.cs
index 7eca49183d..6fcba1ba1a 100644
--- a/src/Umbraco.Tests/TestHelpers/TestHelper.cs
+++ b/src/Umbraco.Tests/TestHelpers/TestHelper.cs
@@ -324,6 +324,8 @@ namespace Umbraco.Tests.TestHelpers
public static IHostingEnvironment GetHostingEnvironment() => _testHelperInternal.GetHostingEnvironment();
+ public static ILoggingConfiguration GetLoggingConfiguration(IHostingEnvironment hostingEnv) => _testHelperInternal.GetLoggingConfiguration(hostingEnv);
+
public static IApplicationShutdownRegistry GetHostingEnvironmentLifetime() => _testHelperInternal.GetHostingEnvironmentLifetime();
public static IIpResolver GetIpResolver() => _testHelperInternal.GetIpResolver();
diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects.cs b/src/Umbraco.Tests/TestHelpers/TestObjects.cs
index c27bd046c8..78c442f688 100644
--- a/src/Umbraco.Tests/TestHelpers/TestObjects.cs
+++ b/src/Umbraco.Tests/TestHelpers/TestObjects.cs
@@ -158,12 +158,12 @@ namespace Umbraco.Tests.TestHelpers
var userService = GetLazyService(factory, c => new UserService(scopeProvider, logger, eventMessagesFactory, runtimeState, GetRepo(c), GetRepo(c),globalSettings));
var dataTypeService = GetLazyService(factory, c => new DataTypeService(scopeProvider, logger, eventMessagesFactory, GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c), ioHelper, localizedTextService.Value, localizationService.Value, TestHelper.ShortStringHelper));
var propertyValidationService = new Lazy(() => new PropertyValidationService(propertyEditorCollection, dataTypeService.Value));
- var contentService = GetLazyService(factory, c => new ContentService(scopeProvider, logger, eventMessagesFactory, GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c), propertyValidationService));
+ var contentService = GetLazyService(factory, c => new ContentService(scopeProvider, logger, eventMessagesFactory, GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c), propertyValidationService, TestHelper.ShortStringHelper));
var notificationService = GetLazyService(factory, c => new NotificationService(scopeProvider, userService.Value, contentService.Value, localizationService.Value, logger, ioHelper, GetRepo(c), globalSettings, contentSettings));
var serverRegistrationService = GetLazyService(factory, c => new ServerRegistrationService(scopeProvider, logger, eventMessagesFactory, GetRepo(c), TestHelper.GetHostingEnvironment()));
var memberGroupService = GetLazyService(factory, c => new MemberGroupService(scopeProvider, logger, eventMessagesFactory, GetRepo(c)));
var memberService = GetLazyService(factory, c => new MemberService(scopeProvider, logger, eventMessagesFactory, memberGroupService.Value, GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c)));
- var mediaService = GetLazyService(factory, c => new MediaService(scopeProvider, mediaFileSystem, logger, eventMessagesFactory, GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c)));
+ var mediaService = GetLazyService(factory, c => new MediaService(scopeProvider, mediaFileSystem, logger, eventMessagesFactory, GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c), TestHelper.ShortStringHelper));
var contentTypeService = GetLazyService(factory, c => new ContentTypeService(scopeProvider, logger, eventMessagesFactory, contentService.Value, GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c)));
var mediaTypeService = GetLazyService(factory, c => new MediaTypeService(scopeProvider, logger, eventMessagesFactory, mediaService.Value, GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c)));
var fileService = GetLazyService(factory, c => new FileService(scopeProvider, ioHelper, logger, eventMessagesFactory, GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c), TestHelper.ShortStringHelper, globalSettings));
diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs
index 901192c609..80f6ab9c9e 100644
--- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs
+++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs
@@ -264,7 +264,7 @@ namespace Umbraco.Tests.Testing
profiler = Mock.Of();
break;
case UmbracoTestOptions.Logger.Serilog:
- logger = new SerilogLogger(TestHelper.CoreDebugSettings, IOHelper, TestHelper.Marchal, new FileInfo(TestHelper.MapPathForTestFiles("~/unit-test.config")));
+ logger = new SerilogLogger(new FileInfo(TestHelper.MapPathForTestFiles("~/unit-test.config")));
profiler = new LogProfiler(logger);
break;
case UmbracoTestOptions.Logger.Console:
diff --git a/src/Umbraco.Tests/UmbracoExamine/ExamineBaseTest.cs b/src/Umbraco.Tests/UmbracoExamine/ExamineBaseTest.cs
index a6c544062e..0d55fd99d7 100644
--- a/src/Umbraco.Tests/UmbracoExamine/ExamineBaseTest.cs
+++ b/src/Umbraco.Tests/UmbracoExamine/ExamineBaseTest.cs
@@ -18,7 +18,7 @@ namespace Umbraco.Tests.UmbracoExamine
public void InitializeFixture()
{
- var logger = new SerilogLogger(TestHelper.CoreDebugSettings, IOHelper, TestHelper.Marchal, new FileInfo(TestHelper.MapPathForTestFiles("~/unit-test.config")));
+ var logger = new SerilogLogger(new FileInfo(TestHelper.MapPathForTestFiles("~/unit-test.config")));
_profilingLogger = new ProfilingLogger(logger, new LogProfiler(logger));
}
diff --git a/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoApplicationBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoApplicationBuilderExtensions.cs
index ddf06e6532..a27113e881 100644
--- a/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoApplicationBuilderExtensions.cs
+++ b/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoApplicationBuilderExtensions.cs
@@ -1,9 +1,13 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
+using Serilog.Context;
using Smidge;
using Umbraco.Core;
+using Umbraco.Core.Configuration;
using Umbraco.Core.Hosting;
+using Umbraco.Infrastructure.Logging.Serilog.Enrichers;
+using Umbraco.Web.Common.Middleware;
namespace Umbraco.Web.BackOffice.AspNetCore
{
@@ -31,6 +35,10 @@ namespace Umbraco.Web.BackOffice.AspNetCore
var runtimeShutdown = new CoreRuntimeShutdown(runtime, hostLifetime);
hostLifetime.RegisterObject(runtimeShutdown);
+ // Register our global threadabort enricher for logging
+ var threadAbortEnricher = app.ApplicationServices.GetRequiredService();
+ LogContext.Push(threadAbortEnricher); // NOTE: We are not in a using clause because we are not removing it, it is on the global context
+
// Start the runtime!
runtime.Start();
@@ -64,6 +72,15 @@ namespace Umbraco.Web.BackOffice.AspNetCore
}
}
+ public static IApplicationBuilder UseUmbracoRequestLogging(this IApplicationBuilder app)
+ {
+ if (app == null) throw new ArgumentNullException(nameof(app));
+
+ app.UseMiddleware();
+
+ return app;
+ }
+
public static IApplicationBuilder UseUmbracoRuntimeMinification(this IApplicationBuilder app)
{
if (app == null) throw new ArgumentNullException(nameof(app));
diff --git a/src/Umbraco.Web.BackOffice/Filters/MinifyJavaScriptResultAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/MinifyJavaScriptResultAttribute.cs
index 0ed0fd658a..65c2be051f 100644
--- a/src/Umbraco.Web.BackOffice/Filters/MinifyJavaScriptResultAttribute.cs
+++ b/src/Umbraco.Web.BackOffice/Filters/MinifyJavaScriptResultAttribute.cs
@@ -2,9 +2,7 @@
using Microsoft.AspNetCore.Mvc.Filters;
using Umbraco.Core.Hosting;
using Microsoft.Extensions.DependencyInjection;
-using Umbraco.Core.Runtime;
using Umbraco.Core.WebAssets;
-using Umbraco.Web.BackOffice.Controllers;
using Umbraco.Web.Common.ActionResults;
namespace Umbraco.Web.BackOffice.Filters
@@ -14,10 +12,11 @@ namespace Umbraco.Web.BackOffice.Filters
public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
// logic before action goes here
- var hostingEnvironment = context.HttpContext.RequestServices.GetService();
+ var serviceProvider = context.HttpContext.RequestServices;
+ var hostingEnvironment = serviceProvider.GetService();
if (!hostingEnvironment.IsDebugMode)
{
- var runtimeMinifier = context.HttpContext.RequestServices.GetService();
+ var runtimeMinifier = serviceProvider.GetService();
if (context.Result is JavaScriptResult jsResult)
{
diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs
index decfcfa660..b3ec11c241 100644
--- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs
+++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs
@@ -5,13 +5,12 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Umbraco.Core;
using Umbraco.Core.Configuration;
+using Umbraco.Core.Logging;
namespace Umbraco.Web.Common.AspNetCore
{
public class AspNetCoreHostingEnvironment : Umbraco.Core.Hosting.IHostingEnvironment
{
-
-
private readonly IHostingSettings _hostingSettings;
private readonly IWebHostEnvironment _webHostEnvironment;
@@ -28,6 +27,7 @@ namespace Umbraco.Web.Common.AspNetCore
ApplicationVirtualPath = "/"; //TODO how to find this, This is a server thing, not application thing.
IISVersion = new Version(0, 0); // TODO not necessary IIS
+
}
public bool IsHosted { get; } = true;
diff --git a/src/Umbraco.Web.Common/Constants/ViewConstants.cs b/src/Umbraco.Web.Common/Constants/ViewConstants.cs
new file mode 100644
index 0000000000..5da1a74f55
--- /dev/null
+++ b/src/Umbraco.Web.Common/Constants/ViewConstants.cs
@@ -0,0 +1,12 @@
+namespace Umbraco.Web.Common.Constants
+{
+ ///
+ /// constants
+ ///
+ internal static class ViewConstants
+ {
+ internal const string ViewLocation = "~/Views";
+
+ internal const string DataTokenCurrentViewContext = "umbraco-current-view-context";
+ }
+}
diff --git a/src/Umbraco.Web.Common/Controllers/RenderController.cs b/src/Umbraco.Web.Common/Controllers/RenderController.cs
new file mode 100644
index 0000000000..43058616de
--- /dev/null
+++ b/src/Umbraco.Web.Common/Controllers/RenderController.cs
@@ -0,0 +1,9 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Umbraco.Web.Common.Controllers
+{
+ public abstract class RenderController : Controller
+ {
+
+ }
+}
diff --git a/src/Umbraco.Web.Common/Events/ActionExecutedEventArgs.cs b/src/Umbraco.Web.Common/Events/ActionExecutedEventArgs.cs
new file mode 100644
index 0000000000..b33cbc7d8a
--- /dev/null
+++ b/src/Umbraco.Web.Common/Events/ActionExecutedEventArgs.cs
@@ -0,0 +1,17 @@
+using System;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Umbraco.Web.Common.Events
+{
+ public class ActionExecutedEventArgs : EventArgs
+ {
+ public Controller Controller { get; set; }
+ public object Model { get; set; }
+
+ public ActionExecutedEventArgs(Controller controller, object model)
+ {
+ Controller = controller;
+ Model = model;
+ }
+ }
+}
diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs
index 843620d571..3facf1b77f 100644
--- a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs
+++ b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs
@@ -1,4 +1,6 @@
using System;
+using System.Collections;
+using System.Collections.Generic;
using System.Data.Common;
using System.IO;
using System.Reflection;
@@ -6,6 +8,10 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Serilog;
+using Serilog.Extensions.Hosting;
+using Serilog.Extensions.Logging;
using Smidge;
using Smidge.Nuglify;
using Umbraco.Composing;
@@ -71,7 +77,22 @@ namespace Umbraco.Web.Common.Extensions
var umbContainer = UmbracoServiceProviderFactory.UmbracoContainer;
- services.AddUmbracoCore(webHostEnvironment, umbContainer, Assembly.GetEntryAssembly(), out factory);
+ var loggingConfig = new LoggingConfiguration(
+ Path.Combine(webHostEnvironment.ContentRootPath, "App_Data\\Logs"),
+ Path.Combine(webHostEnvironment.ContentRootPath, "config\\serilog.config"),
+ Path.Combine(webHostEnvironment.ContentRootPath, "config\\serilog.user.config"));
+
+ IHttpContextAccessor httpContextAccessor = new HttpContextAccessor();
+ services.AddSingleton(httpContextAccessor);
+
+ var requestCache = new GenericDictionaryRequestAppCache(() => httpContextAccessor.HttpContext.Items);
+
+ services.AddUmbracoCore(webHostEnvironment,
+ umbContainer,
+ Assembly.GetEntryAssembly(),
+ requestCache,
+ loggingConfig,
+ out factory);
return services;
}
@@ -83,20 +104,34 @@ namespace Umbraco.Web.Common.Extensions
///
///
///
+ ///
+ ///
+ ///
///
///
- public static IServiceCollection AddUmbracoCore(this IServiceCollection services, IWebHostEnvironment webHostEnvironment, IRegister umbContainer, Assembly entryAssembly, out IFactory factory)
+ public static IServiceCollection AddUmbracoCore(
+ this IServiceCollection services,
+ IWebHostEnvironment webHostEnvironment,
+ IRegister umbContainer,
+ Assembly entryAssembly,
+ IRequestCache requestCache,
+ ILoggingConfiguration loggingConfiguration,
+ out IFactory factory)
{
if (services is null) throw new ArgumentNullException(nameof(services));
var container = umbContainer;
if (container is null) throw new ArgumentNullException(nameof(container));
if (entryAssembly is null) throw new ArgumentNullException(nameof(entryAssembly));
- // Special case! The generic host adds a few default services but we need to manually add this one here NOW because
- // we resolve it before the host finishes configuring in the call to CreateCompositionRoot
- services.AddSingleton();
+ var serviceProvider = services.BuildServiceProvider();
+ var configs = serviceProvider.GetService();
- CreateCompositionRoot(services, webHostEnvironment, out var logger, out var configs, out var ioHelper, out var hostingEnvironment, out var backOfficeInfo, out var profiler);
+
+ CreateCompositionRoot(services,
+ configs,
+ webHostEnvironment,
+ loggingConfiguration,
+ out var logger, out var ioHelper, out var hostingEnvironment, out var backOfficeInfo, out var profiler);
var globalSettings = configs.Global();
var umbracoVersion = new UmbracoVersion(globalSettings);
@@ -109,14 +144,15 @@ namespace Umbraco.Web.Common.Extensions
profiler,
hostingEnvironment,
backOfficeInfo,
- CreateTypeFinder(logger, profiler, webHostEnvironment, entryAssembly));
+ CreateTypeFinder(logger, profiler, webHostEnvironment, entryAssembly),
+ requestCache);
factory = coreRuntime.Configure(container);
return services;
}
- private static ITypeFinder CreateTypeFinder(ILogger logger, IProfiler profiler, IWebHostEnvironment webHostEnvironment, Assembly entryAssembly)
+ private static ITypeFinder CreateTypeFinder(Core.Logging.ILogger logger, IProfiler profiler, IWebHostEnvironment webHostEnvironment, Assembly entryAssembly)
{
// TODO: Currently we are not passing in any TypeFinderConfig (with ITypeFinderSettings) which we should do, however
// this is not critical right now and would require loading in some config before boot time so just leaving this as-is for now.
@@ -126,11 +162,12 @@ namespace Umbraco.Web.Common.Extensions
return new TypeFinder(logger, new DefaultUmbracoAssemblyProvider(entryAssembly), runtimeHash);
}
- private static IRuntime GetCoreRuntime(Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, ILogger logger,
+ private static IRuntime GetCoreRuntime(
+ Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, Core.Logging.ILogger logger,
IProfiler profiler, Core.Hosting.IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo,
- ITypeFinder typeFinder)
+ ITypeFinder typeFinder, IRequestCache requestCache)
{
- var connectionStringConfig = configs.ConnectionStrings()[Constants.System.UmbracoConnectionName];
+ var connectionStringConfig = configs.ConnectionStrings()[Core.Constants.System.UmbracoConnectionName];
var dbProviderFactoryCreator = new SqlServerDbProviderFactoryCreator(
connectionStringConfig?.ProviderName,
DbProviderFactories.GetFactory);
@@ -145,53 +182,92 @@ namespace Umbraco.Web.Common.Extensions
var mainDom = new MainDom(logger, mainDomLock);
- var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetCoreBootPermissionsChecker(),
- hostingEnvironment, backOfficeInfo, dbProviderFactoryCreator, mainDom, typeFinder);
+ var coreRuntime = new CoreRuntime(
+ configs,
+ umbracoVersion,
+ ioHelper,
+ logger,
+ profiler,
+ new AspNetCoreBootPermissionsChecker(),
+ hostingEnvironment,
+ backOfficeInfo,
+ dbProviderFactoryCreator,
+ mainDom,
+ typeFinder,
+ requestCache);
return coreRuntime;
}
- private static IServiceCollection CreateCompositionRoot(IServiceCollection services, IWebHostEnvironment webHostEnvironment,
- out ILogger logger, out Configs configs, out IIOHelper ioHelper, out Core.Hosting.IHostingEnvironment hostingEnvironment,
- out IBackOfficeInfo backOfficeInfo, out IProfiler profiler)
+ private static IServiceCollection CreateCompositionRoot(
+ IServiceCollection services,
+ Configs configs,
+ IWebHostEnvironment webHostEnvironment,
+ ILoggingConfiguration loggingConfiguration,
+ out Core.Logging.ILogger logger,
+ out IIOHelper ioHelper,
+ out Core.Hosting.IHostingEnvironment hostingEnvironment,
+ out IBackOfficeInfo backOfficeInfo,
+ out IProfiler profiler)
{
- // TODO: We need to avoid this, surely there's a way? See ContainerTests.BuildServiceProvider_Before_Host_Is_Configured
- var serviceProvider = services.BuildServiceProvider();
-
- var httpContextAccessor = serviceProvider.GetRequiredService();
-
- configs = serviceProvider.GetService();
if (configs == null)
throw new InvalidOperationException($"Could not resolve type {typeof(Configs)} from the container, ensure {nameof(AddUmbracoConfiguration)} is called before calling {nameof(AddUmbracoCore)}");
var hostingSettings = configs.Hosting();
- var coreDebug = configs.CoreDebug();
var globalSettings = configs.Global();
hostingEnvironment = new AspNetCoreHostingEnvironment(hostingSettings, webHostEnvironment);
ioHelper = new IOHelper(hostingEnvironment, globalSettings);
- logger = SerilogLogger.CreateWithDefaultConfiguration(hostingEnvironment,
- new AspNetCoreSessionManager(httpContextAccessor),
- // TODO: We need to avoid this, surely there's a way? See ContainerTests.BuildServiceProvider_Before_Host_Is_Configured
- () => services.BuildServiceProvider().GetService(), coreDebug, ioHelper,
- new AspNetCoreMarchal());
+ logger = AddLogger(services, hostingEnvironment, loggingConfiguration);
backOfficeInfo = new AspNetCoreBackOfficeInfo(globalSettings);
- profiler = GetWebProfiler(hostingEnvironment, httpContextAccessor);
+ profiler = GetWebProfiler(hostingEnvironment);
return services;
}
+ ///
+ /// Create and configure the logger
+ ///
+ ///
+ private static Core.Logging.ILogger AddLogger(IServiceCollection services, Core.Hosting.IHostingEnvironment hostingEnvironment, ILoggingConfiguration loggingConfiguration)
+ {
+ // Create a serilog logger
+ var logger = SerilogLogger.CreateWithDefaultConfiguration(hostingEnvironment, loggingConfiguration);
+
+ // Wire up all the bits that serilog needs. We need to use our own code since the Serilog ext methods don't cater to our needs since
+ // we don't want to use the global serilog `Log` object and we don't have our own ILogger implementation before the HostBuilder runs which
+ // is the only other option that these ext methods allow.
+ // I have created a PR to make this nicer https://github.com/serilog/serilog-extensions-hosting/pull/19 but we'll need to wait for that.
+ // Also see : https://github.com/serilog/serilog-extensions-hosting/blob/dev/src/Serilog.Extensions.Hosting/SerilogHostBuilderExtensions.cs
+
+ services.AddSingleton(services => new SerilogLoggerFactory(logger.SerilogLog, false));
+
+ // This won't (and shouldn't) take ownership of the logger.
+ services.AddSingleton(logger.SerilogLog);
+
+ // Registered to provide two services...
+ var diagnosticContext = new DiagnosticContext(logger.SerilogLog);
+
+ // Consumed by e.g. middleware
+ services.AddSingleton(diagnosticContext);
+
+ // Consumed by user code
+ services.AddSingleton