diff --git a/src/Umbraco.Core/Composing/TypeLoader.cs b/src/Umbraco.Core/Composing/TypeLoader.cs
index 2b55f78860..ef72386135 100644
--- a/src/Umbraco.Core/Composing/TypeLoader.cs
+++ b/src/Umbraco.Core/Composing/TypeLoader.cs
@@ -4,14 +4,11 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
-using System.Text;
-using System.Threading;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Collections;
using Umbraco.Cms.Core.Logging;
using Umbraco.Extensions;
-using File = System.IO.File;
namespace Umbraco.Cms.Core.Composing
{
@@ -21,112 +18,58 @@ namespace Umbraco.Cms.Core.Composing
///
/// This class should be used to get all types, the class should never be used directly.
/// In most cases this class is not used directly but through extension methods that retrieve specific types.
- /// This class caches the types it knows to avoid excessive assembly scanning and shorten startup times, relying
- /// on a hash of the DLLs in the ~/bin folder to check for cache expiration.
///
public sealed class TypeLoader
{
- private readonly IRuntimeHash _runtimeHash;
- private readonly IAppPolicyCache _runtimeCache;
private readonly ILogger _logger;
- private readonly IProfilingLogger _profilingLogger;
- private readonly Dictionary _types = new Dictionary();
- private readonly object _locko = new object();
- private readonly object _timerLock = new object();
+ private readonly Dictionary _types = new ();
+ private readonly object _locko = new ();
- private Timer? _timer;
- private bool _timing;
- private string? _cachedAssembliesHash;
- private string? _currentAssembliesHash;
- private IEnumerable? _assemblies;
- private bool _reportedChange;
- private readonly DirectoryInfo _localTempPath;
- private readonly Lazy _fileBasePath;
- private readonly Dictionary<(string, string), IEnumerable> _emptyCache = new Dictionary<(string, string), IEnumerable>();
- private string? _typesListFilePath;
- private string? _typesHashFilePath;
+ private IEnumerable _assemblies;
///
/// Initializes a new instance of the class.
///
- ///
- /// The application runtime cache.
- /// Files storage location.
- /// A profiling logger.
- ///
- public TypeLoader(ITypeFinder typeFinder, IRuntimeHash runtimeHash, IAppPolicyCache runtimeCache, DirectoryInfo localTempPath, ILogger logger, IProfiler profiler, IEnumerable? assembliesToScan = null)
- : this(typeFinder, runtimeHash, runtimeCache, localTempPath, logger, profiler, true, assembliesToScan)
- { }
-
- ///
- /// Initializes a new instance of the class.
- ///
- ///
- /// The application runtime cache.
- /// Files storage location.
- /// A profiling logger.
- /// Whether to detect changes using hashes.
- ///
- public TypeLoader(ITypeFinder typeFinder, IRuntimeHash runtimeHash, IAppPolicyCache runtimeCache, DirectoryInfo localTempPath, ILogger logger, IProfiler profiler, bool detectChanges, IEnumerable? assembliesToScan = null)
+ [Obsolete("Please use an alternative constructor.")]
+ public TypeLoader(
+ ITypeFinder typeFinder,
+ IRuntimeHash runtimeHash,
+ IAppPolicyCache runtimeCache,
+ DirectoryInfo localTempPath,
+ ILogger logger,
+ IProfiler profiler,
+ IEnumerable? assembliesToScan = null)
+ : this(typeFinder, logger, assembliesToScan)
{
- if (profiler is null)
- {
- throw new ArgumentNullException(nameof(profiler));
- }
-
- var runtimeHashValue = runtimeHash.GetHashValue();
- CacheKey = runtimeHashValue + "umbraco-types.list";
-
- TypeFinder = typeFinder ?? throw new ArgumentNullException(nameof(typeFinder));
- _runtimeHash = runtimeHash;
- _runtimeCache = runtimeCache ?? throw new ArgumentNullException(nameof(runtimeCache));
- _localTempPath = localTempPath;
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- _profilingLogger = new ProfilingLogger(logger, profiler);
- _assemblies = assembliesToScan;
-
- _fileBasePath = new Lazy(GetFileBasePath);
-
- if (detectChanges)
- {
- //first check if the cached hash is string.Empty, if it is then we need
- //do the check if they've changed
- RequiresRescanning = CachedAssembliesHash != CurrentAssembliesHash || CachedAssembliesHash == string.Empty;
- //if they have changed, we need to write the new file
- if (RequiresRescanning)
- {
- _logger.LogDebug("Plugin types are being re-scanned. Cached hash value: {CachedHash}, Current hash value: {CurrentHash}", CachedAssembliesHash, CurrentAssembliesHash);
-
- // if the hash has changed, clear out the persisted list no matter what, this will force
- // rescanning of all types including lazy ones.
- // http://issues.umbraco.org/issue/U4-4789
- var typesListFilePath = GetTypesListFilePath();
- if (typesListFilePath != null)
- {
- DeleteFile(typesListFilePath, FileDeleteTimeout);
- }
-
- WriteCacheTypesHash();
- }
- }
- else
- {
- // if the hash has changed, clear out the persisted list no matter what, this will force
- // rescanning of all types including lazy ones.
- // http://issues.umbraco.org/issue/U4-4789
- var typesListFilePath = GetTypesListFilePath();
- if (typesListFilePath != null)
- {
- DeleteFile(typesListFilePath, FileDeleteTimeout);
- }
-
- // always set to true if we're not detecting (generally only for testing)
- RequiresRescanning = true;
- }
}
- internal string CacheKey { get; }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ [Obsolete("Please use an alternative constructor.")]
+ public TypeLoader(
+ ITypeFinder typeFinder,
+ IRuntimeHash runtimeHash,
+ IAppPolicyCache runtimeCache,
+ DirectoryInfo localTempPath,
+ ILogger logger,
+ IProfiler profiler,
+ bool detectChanges,
+ IEnumerable? assembliesToScan = null)
+ : this(typeFinder, logger, assembliesToScan)
+ {
+ }
+
+ public TypeLoader(
+ ITypeFinder typeFinder,
+ ILogger logger,
+ IEnumerable? assembliesToScan = null)
+ {
+ TypeFinder = typeFinder ?? throw new ArgumentNullException(nameof(typeFinder));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _assemblies = assembliesToScan;
+ }
///
/// Returns the underlying
@@ -144,6 +87,7 @@ namespace Umbraco.Cms.Core.Composing
/// This is for unit tests.
///
// internal for tests
+ [Obsolete("This will be removed in a future version.")]
public IEnumerable AssembliesToScan => _assemblies ??= TypeFinder.AssembliesToScan;
///
@@ -151,6 +95,7 @@ namespace Umbraco.Cms.Core.Composing
///
/// For unit tests.
// internal for tests
+ [Obsolete("This will be removed in a future version.")]
public IEnumerable TypeLists => _types.Values;
///
@@ -158,378 +103,49 @@ namespace Umbraco.Cms.Core.Composing
///
/// For unit tests.
// internal for tests
+ [Obsolete("This will be removed in a future version.")]
public void AddTypeList(TypeList typeList)
{
var tobject = typeof(object); // CompositeTypeTypeKey does not support null values
_types[new CompositeTypeTypeKey(typeList.BaseType ?? tobject, typeList.AttributeType ?? tobject)] = typeList;
}
- #region Hashing
-
- ///
- /// Gets a value indicating whether the assemblies in bin, app_code, global.asax, etc... have changed since they were last hashed.
- ///
- private bool RequiresRescanning { get; }
-
- ///
- /// Gets the currently cached hash value of the scanned assemblies.
- ///
- /// The cached hash value, or string.Empty if no cache is found.
- internal string CachedAssembliesHash
- {
- get
- {
- if (_cachedAssembliesHash != null)
- {
- return _cachedAssembliesHash;
- }
-
- var typesHashFilePath = GetTypesHashFilePath();
- if (typesHashFilePath == null)
- {
- return string.Empty;
- }
-
- if (!File.Exists(typesHashFilePath))
- {
- return string.Empty;
- }
-
- var hash = File.ReadAllText(typesHashFilePath, Encoding.UTF8);
-
- _cachedAssembliesHash = hash;
- return _cachedAssembliesHash;
- }
- }
-
- ///
- /// Gets the current assemblies hash based on creating a hash from the assemblies in various places.
- ///
- /// The current hash.
- private string CurrentAssembliesHash
- {
- get
- {
- if (_currentAssembliesHash != null)
- {
- return _currentAssembliesHash;
- }
-
- _currentAssembliesHash = _runtimeHash.GetHashValue();
-
- return _currentAssembliesHash;
- }
- }
-
- ///
- /// Writes the assembly hash file.
- ///
- private void WriteCacheTypesHash()
- {
- var typesHashFilePath = GetTypesHashFilePath();
- if (typesHashFilePath != null)
- {
- File.WriteAllText(typesHashFilePath, CurrentAssembliesHash, Encoding.UTF8);
- }
- }
-
- #endregion
-
#region Cache
- private const int ListFileOpenReadTimeout = 4000; // milliseconds
- private const int ListFileOpenWriteTimeout = 2000; // milliseconds
- private const int ListFileWriteThrottle = 500; // milliseconds - throttle before writing
- private const int ListFileCacheDuration = 2 * 60; // seconds - duration we cache the entire list
- private const int FileDeleteTimeout = 4000; // milliseconds
-
// internal for tests
- public Attempt?> TryGetCached(Type? baseType, Type? attributeType)
+ [Obsolete("This will be removed in a future version.")]
+ public Attempt> TryGetCached(Type baseType, Type attributeType)
{
- var cache =
- _runtimeCache.GetCacheItem(CacheKey, ReadCacheSafe, TimeSpan.FromSeconds(ListFileCacheDuration))!;
-
- cache.TryGetValue(
- (baseType == null ? string.Empty : baseType.FullName ?? string.Empty, attributeType == null ? string.Empty : attributeType.FullName ?? string.Empty),
- out IEnumerable? types);
-
- return types == null
- ? Attempt?>.Fail()
- : Attempt.Succeed(types);
- }
-
- private Dictionary<(string, string), IEnumerable> ReadCacheSafe()
- {
- try
- {
- return ReadCache();
- }
- catch
- {
- try
- {
- var typesListFilePath = GetTypesListFilePath();
- if (typesListFilePath != null)
- {
- DeleteFile(typesListFilePath, FileDeleteTimeout);
- }
- }
- catch
- {
- // on-purpose, does not matter
- }
- }
-
- return _emptyCache;
+ return Attempt>.Fail();
}
// internal for tests
- public Dictionary<(string, string), IEnumerable> ReadCache()
- {
- var typesListFilePath = GetTypesListFilePath();
- if (typesListFilePath == null || File.Exists(typesListFilePath) == false)
- {
- return _emptyCache;
- }
-
- var cache = new Dictionary<(string, string), IEnumerable>();
- using (var stream = GetFileStream(typesListFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, ListFileOpenReadTimeout))
- using (var reader = new StreamReader(stream))
- {
- while (true)
- {
- var baseType = reader.ReadLine();
- if (baseType == null)
- {
- return cache; // exit
- }
-
- if (baseType.StartsWith("<"))
- {
- break; // old xml
- }
-
- var attributeType = reader.ReadLine();
- if (attributeType == null)
- {
- break;
- }
-
- var types = new List();
- while (true)
- {
- var type = reader.ReadLine();
- if (type == null)
- {
- types = null; // break 2 levels
- break;
- }
- if (type == string.Empty)
- {
- cache[(baseType, attributeType)] = types;
- break;
- }
- types.Add(type);
- }
-
- if (types == null)
- {
- break;
- }
- }
- }
-
- cache.Clear();
- return cache;
- }
+ [Obsolete("This will be removed in a future version.")]
+ public Dictionary<(string, string), IEnumerable> ReadCache() => null;
// internal for tests
- public string? GetTypesListFilePath() => _typesListFilePath ??= _fileBasePath.Value == null ? null : _fileBasePath.Value + ".list";
-
- private string? GetTypesHashFilePath() => _typesHashFilePath ??= _fileBasePath.Value == null ? null : _fileBasePath.Value + ".hash";
-
- ///
- /// Used to produce the Lazy value of _fileBasePath
- ///
- ///
- private string? GetFileBasePath()
- {
- if (_localTempPath == null)
- {
- return null;
- }
-
- var fileBasePath = Path.Combine(_localTempPath.FullName, "TypesCache", "umbraco-types." + EnvironmentHelper.FileSafeMachineName);
-
- // ensure that the folder exists
- var directory = Path.GetDirectoryName(fileBasePath);
- if (directory == null)
- {
- throw new InvalidOperationException($"Could not determine folder for path \"{fileBasePath}\".");
- }
-
- if (Directory.Exists(directory) == false)
- {
- Directory.CreateDirectory(directory);
- }
-
- return fileBasePath;
- }
+ [Obsolete("This will be removed in a future version.")]
+ public string GetTypesListFilePath() => null;
// internal for tests
+ [Obsolete("This will be removed in a future version.")]
public void WriteCache()
{
- _logger.LogDebug("Writing cache file.");
- var typesListFilePath = GetTypesListFilePath();
- if (typesListFilePath == null)
- {
- return;
- }
-
- using (var stream = GetFileStream(typesListFilePath, FileMode.Create, FileAccess.Write, FileShare.None, ListFileOpenWriteTimeout))
- using (var writer = new StreamWriter(stream))
- {
- foreach (var typeList in _types.Values)
- {
- writer.WriteLine(typeList.BaseType == null ? string.Empty : typeList.BaseType.FullName);
- writer.WriteLine(typeList.AttributeType == null ? string.Empty : typeList.AttributeType.FullName);
- foreach (var type in typeList.Types)
- {
- writer.WriteLine(type.AssemblyQualifiedName);
- }
-
- writer.WriteLine();
- }
- }
- }
-
- // internal for tests
- internal void UpdateCache()
- {
- void TimerRelease(object? o)
- {
- lock (_timerLock)
- {
- try
- {
- WriteCache();
- }
- catch { /* bah - just don't die */ }
- if (!_timing)
- _timer = null;
- }
- }
-
- lock (_timerLock)
- {
- if (_timer == null)
- {
- _timer = new Timer(TimerRelease, null, ListFileWriteThrottle, Timeout.Infinite);
- }
- else
- {
- _timer.Change(ListFileWriteThrottle, Timeout.Infinite);
- }
-
- _timing = true;
- }
}
///
- /// Removes cache files and internal cache.
+ /// Clears cache.
///
/// Generally only used for resetting cache, for example during the install process.
+ [Obsolete("This will be removed in a future version.")]
public void ClearTypesCache()
{
- var typesListFilePath = GetTypesListFilePath();
- if (typesListFilePath == null)
- {
- return;
- }
-
- DeleteFile(typesListFilePath, FileDeleteTimeout);
-
- var typesHashFilePath = GetTypesHashFilePath();
- if (typesHashFilePath != null)
- {
- DeleteFile(typesHashFilePath, FileDeleteTimeout);
- }
-
- _runtimeCache.Clear(CacheKey);
- }
-
- private Stream GetFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare fileShare, int timeoutMilliseconds)
- {
- const int pauseMilliseconds = 250;
- var attempts = timeoutMilliseconds / pauseMilliseconds;
- while (true)
- {
- try
- {
- return new FileStream(path, fileMode, fileAccess, fileShare);
- }
- catch
- {
- if (--attempts == 0)
- {
- throw;
- }
-
- _logger.LogDebug("Attempted to get filestream for file {Path} failed, {NumberOfAttempts} attempts left, pausing for {PauseMilliseconds} milliseconds", path, attempts, pauseMilliseconds);
- Thread.Sleep(pauseMilliseconds);
- }
- }
- }
-
- private void DeleteFile(string path, int timeoutMilliseconds)
- {
- const int pauseMilliseconds = 250;
- var attempts = timeoutMilliseconds / pauseMilliseconds;
- while (File.Exists(path))
- {
- try
- {
- File.Delete(path);
- }
- catch
- {
- if (--attempts == 0)
- throw;
-
- _logger.LogDebug("Attempted to delete file {Path} failed, {NumberOfAttempts} attempts left, pausing for {PauseMilliseconds} milliseconds", path, attempts, pauseMilliseconds);
- Thread.Sleep(pauseMilliseconds);
- }
- }
}
#endregion
#region Get Assembly Attributes
- /////
- ///// Gets the assembly attributes of the specified type .
- /////
- ///// The attribute type.
- /////
- ///// The assembly attributes of the specified type .
- /////
- //public IEnumerable GetAssemblyAttributes()
- // where T : Attribute
- //{
- // return AssembliesToScan.SelectMany(a => a.GetCustomAttributes()).ToList();
- //}
-
- /////
- ///// Gets all the assembly attributes.
- /////
- /////
- ///// All assembly attributes.
- /////
- //public IEnumerable GetAssemblyAttributes()
- //{
- // return AssembliesToScan.SelectMany(a => a.GetCustomAttributes()).ToList();
- //}
-
///
/// Gets the assembly attributes of the specified .
///
@@ -701,17 +317,9 @@ namespace Umbraco.Cms.Core.Composing
// loader is mostly not going to be used in any kind of massively multi-threaded scenario - so,
// a plain lock is enough
- var name = GetName(baseType, attributeType);
-
lock (_locko)
{
- using (_profilingLogger.DebugDuration(
- "Getting " + name,
- "Got " + name)) // cannot contain typesFound.Count as it's evaluated before the find
- {
- // get within a lock & timer
- return GetTypesInternalLocked(baseType, attributeType, finder, action, cache);
- }
+ return GetTypesInternalLocked(baseType, attributeType, finder, action, cache);
}
}
@@ -731,9 +339,9 @@ namespace Umbraco.Cms.Core.Composing
{
// check if the TypeList already exists, if so return it, if not we'll create it
var tobject = typeof(object); // CompositeTypeTypeKey does not support null values
-
var listKey = new CompositeTypeTypeKey(baseType ?? tobject, attributeType ?? tobject);
- TypeList? typeList = null;
+ TypeList typeList = null;
+
if (cache)
{
_types.TryGetValue(listKey, out typeList); // else null
@@ -750,68 +358,12 @@ namespace Umbraco.Cms.Core.Composing
// else proceed,
typeList = new TypeList(baseType, attributeType);
- var typesListFilePath = GetTypesListFilePath();
- var scan = RequiresRescanning || typesListFilePath == null || File.Exists(typesListFilePath) == false;
+ // either we had to scan, or we could not get the types from the cache file - scan now
+ _logger.LogDebug("Getting {TypeName}: " + action + ".", GetName(baseType, attributeType));
- if (scan)
+ foreach (var t in finder())
{
- // either we have to rescan, or we could not find the cache file:
- // report (only once) and scan and update the cache file
- if (_reportedChange == false)
- {
- _logger.LogDebug("Assemblies changes detected, need to rescan everything.");
- _reportedChange = true;
- }
- }
-
- if (scan == false)
- {
- // if we don't have to scan, try the cache
- var cacheResult = TryGetCached(baseType, attributeType);
-
- // here we need to identify if the CachedTypeNotFoundInFile was the exception, if it was then we need to re-scan
- // in some cases the type will not have been scanned for on application startup, but the assemblies haven't changed
- // so in this instance there will never be a result.
- if (cacheResult.Exception is CachedTypeNotFoundInFileException || cacheResult.Success == false)
- {
- _logger.LogDebug("Getting {TypeName}: failed to load from cache file, must scan assemblies.", GetName(baseType, attributeType));
- scan = true;
- }
- else
- {
- // successfully retrieved types from the file cache: load
- foreach (var type in cacheResult.Result!)
- {
- var resolvedType = TypeFinder.GetTypeByName(type);
- if (resolvedType != null)
- {
- typeList.Add(resolvedType);
- }
- else
- {
- // in case of any exception, we have to exit, and revert to scanning
- _logger.LogWarning("Getting {TypeName}: failed to load cache file type {CacheType}, reverting to scanning assemblies.", GetName(baseType, attributeType), type);
- scan = true;
- break;
- }
- }
-
- if (scan == false)
- {
- _logger.LogDebug("Getting {TypeName}: loaded types from cache file.", GetName(baseType, attributeType));
- }
- }
- }
-
- if (scan)
- {
- // either we had to scan, or we could not get the types from the cache file - scan now
- _logger.LogDebug("Getting {TypeName}: " + action + ".", GetName(baseType, attributeType));
-
- foreach (var t in finder())
- {
- typeList.Add(t);
- }
+ typeList.Add(t);
}
// if we are to cache the results, do so
@@ -821,11 +373,6 @@ namespace Umbraco.Cms.Core.Composing
if (added)
{
_types[listKey] = typeList;
- //if we are scanning then update the cache file
- if (scan)
- {
- UpdateCache();
- }
}
_logger.LogDebug("Got {TypeName}, caching ({CacheType}).", GetName(baseType, attributeType), added.ToString().ToLowerInvariant());
@@ -864,7 +411,7 @@ namespace Umbraco.Cms.Core.Composing
///
public void Add(Type type)
{
- if (BaseType?.IsAssignableFrom(type) == false)
+ if (BaseType.IsAssignableFrom(type) == false)
throw new ArgumentException("Base type " + BaseType + " is not assignable from type " + type + ".", nameof(type));
_types.Add(type);
}
diff --git a/src/Umbraco.Core/DependencyInjection/IUmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/IUmbracoBuilder.cs
index 02b356a244..521f55dbef 100644
--- a/src/Umbraco.Core/DependencyInjection/IUmbracoBuilder.cs
+++ b/src/Umbraco.Core/DependencyInjection/IUmbracoBuilder.cs
@@ -1,3 +1,4 @@
+using System;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -27,6 +28,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
///
/// This may be null.
///
+ [Obsolete("This property will be removed in a future version, please find an alternative approach.")]
IHostingEnvironment? BuilderHostingEnvironment { get; }
IProfiler Profiler { get; }
diff --git a/src/Umbraco.Core/Extensions/HostEnvironmentExtensions.cs b/src/Umbraco.Core/Extensions/HostEnvironmentExtensions.cs
new file mode 100644
index 0000000000..658739ba93
--- /dev/null
+++ b/src/Umbraco.Core/Extensions/HostEnvironmentExtensions.cs
@@ -0,0 +1,53 @@
+using System;
+using System.IO;
+using Microsoft.Extensions.Hosting;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Core.Extensions
+{
+ ///
+ /// Contains extension methods for the interface.
+ ///
+ public static class HostEnvironmentExtensions
+ {
+ private static string s_temporaryApplicationId;
+
+ ///
+ /// Maps a virtual path to a physical path to the application's content root.
+ ///
+ ///
+ /// Generally the content root is the parent directory of the web root directory.
+ ///
+ public static string MapPathContentRoot(this IHostEnvironment hostEnvironment, string path)
+ {
+ var root = hostEnvironment.ContentRootPath;
+
+ var newPath = path.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar);
+
+ // TODO: This is a temporary error because we switched from IOHelper.MapPath to HostingEnvironment.MapPathXXX
+ // IOHelper would check if the path passed in started with the root, and not prepend the root again if it did,
+ // however if you are requesting a path be mapped, it should always assume the path is relative to the root, not
+ // absolute in the file system. This error will help us find and fix improper uses, and should be removed once
+ // all those uses have been found and fixed
+ if (newPath.StartsWith(root))
+ {
+ throw new ArgumentException("The path appears to already be fully qualified. Please remove the call to MapPathContentRoot");
+ }
+
+ return Path.Combine(root, newPath.TrimStart(Constants.CharArrays.TildeForwardSlashBackSlash));
+ }
+
+ ///
+ /// Gets a temporary application id for use before the ioc container is built.
+ ///
+ public static string GetTemporaryApplicationId(this IHostEnvironment hostEnvironment)
+ {
+ if (s_temporaryApplicationId != null)
+ {
+ return s_temporaryApplicationId;
+ }
+
+ return s_temporaryApplicationId = hostEnvironment.ContentRootPath.GenerateHash();
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Hosting/IHostingEnvironment.cs b/src/Umbraco.Core/Hosting/IHostingEnvironment.cs
index 5b761211ac..c2c7cfe792 100644
--- a/src/Umbraco.Core/Hosting/IHostingEnvironment.cs
+++ b/src/Umbraco.Core/Hosting/IHostingEnvironment.cs
@@ -15,12 +15,21 @@ namespace Umbraco.Cms.Core.Hosting
/// between restarts of that Umbraco website/application on that specific server.
///
///
- /// The value of this does not necesarily distinguish between unique workers/servers for this Umbraco application.
+ /// The value of this does not distinguish between unique workers/servers for this Umbraco application.
/// Usage of this must take into account that the same may be returned for the same
- /// Umbraco website hosted on different servers. Similarly the usage of this must take into account that a different
+ /// Umbraco website hosted on different servers.
+ /// Similarly the usage of this must take into account that a different
/// may be returned for the same Umbraco website hosted on different servers.
///
+ ///
+ /// This returns a hash of the value of IApplicationDiscriminator.Discriminator (which is most likely just the value of unless an alternative implementation of IApplicationDiscriminator has been registered).
+ /// However during ConfigureServices a temporary instance of IHostingEnvironment is constructed which guarantees that this will be the hash of , so the value may differ depend on when the property is used.
+ ///
+ ///
+ /// If you require this value during ConfigureServices it is probably a code smell.
+ ///
///
+ [Obsolete("Please use IApplicationDiscriminator.Discriminator instead.")]
string ApplicationId { get; }
///
@@ -58,6 +67,7 @@ namespace Umbraco.Cms.Core.Hosting
/// 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.
///
+ [Obsolete("Please use the MapPathWebRoot extension method on an instance of IWebHostEnvironment instead")]
string MapPathWebRoot(string path);
///
@@ -67,6 +77,7 @@ namespace Umbraco.Cms.Core.Hosting
/// 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.
///
+ [Obsolete("Please use the MapPathContentRoot extension method on an instance of IHostEnvironment (or IWebHostEnvironment) instead")]
string MapPathContentRoot(string path);
///
diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs
index 80cbfc28d3..733410cf91 100644
--- a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs
+++ b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs
@@ -2,13 +2,14 @@ using System;
using System.IO;
using System.Text;
using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
using Serilog;
using Serilog.Configuration;
using Serilog.Core;
using Serilog.Events;
using Serilog.Formatting;
using Serilog.Formatting.Compact;
-using Umbraco.Cms.Core.Hosting;
+using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Logging;
using Umbraco.Cms.Core.Logging.Serilog.Enrichers;
using Umbraco.Cms.Infrastructure.Logging.Serilog;
@@ -17,16 +18,15 @@ namespace Umbraco.Extensions
{
public static class LoggerConfigExtensions
{
- private const string AppDomainId = "AppDomainId";
-
///
/// This configures Serilog with some defaults
/// Such as adding ProcessID, Thread, AppDomain etc
/// It is highly recommended that you keep/use this default in your own logging config customizations
///
+ [Obsolete("Please use an alternative method.")]
public static LoggerConfiguration MinimalConfiguration(
this LoggerConfiguration logConfig,
- IHostingEnvironment hostingEnvironment,
+ Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment,
ILoggingConfiguration loggingConfiguration,
IConfiguration configuration)
{
@@ -38,9 +38,10 @@ namespace Umbraco.Extensions
/// Such as adding ProcessID, Thread, AppDomain etc
/// It is highly recommended that you keep/use this default in your own logging config customizations
///
+ [Obsolete("Please use an alternative method.")]
public static LoggerConfiguration MinimalConfiguration(
this LoggerConfiguration logConfig,
- IHostingEnvironment hostingEnvironment,
+ Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment,
ILoggingConfiguration loggingConfiguration,
IConfiguration configuration,
out UmbracoFileConfiguration umbFileConfiguration)
@@ -49,7 +50,7 @@ namespace Umbraco.Extensions
//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", hostingEnvironment.MapPathContentRoot("/").TrimEnd("\\"), EnvironmentVariableTarget.Process);
+ Environment.SetEnvironmentVariable("BASEDIR", hostingEnvironment.MapPathContentRoot("/").TrimEnd(Path.DirectorySeparatorChar), EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable("UMBLOGDIR", loggingConfiguration.LogDirectory, EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable("MACHINENAME", Environment.MachineName, EnvironmentVariableTarget.Process);
@@ -57,8 +58,7 @@ namespace Umbraco.Extensions
.Enrich.WithProcessId()
.Enrich.WithProcessName()
.Enrich.WithThreadId()
- .Enrich.WithProperty(AppDomainId, AppDomain.CurrentDomain.Id)
- .Enrich.WithProperty("AppDomainAppId", hostingEnvironment.ApplicationId.ReplaceNonAlphanumericChars(string.Empty))
+ .Enrich.WithProperty("ApplicationId", hostingEnvironment.ApplicationId) // Updated later by ApplicationIdEnricher
.Enrich.WithProperty("MachineName", Environment.MachineName)
.Enrich.With()
.Enrich.FromLogContext(); // allows us to dynamically enrich
@@ -81,6 +81,48 @@ namespace Umbraco.Extensions
return logConfig;
}
+
+ ///
+ /// This configures Serilog with some defaults
+ /// Such as adding ProcessID, Thread, AppDomain etc
+ /// It is highly recommended that you keep/use this default in your own logging config customizations
+ ///
+ public static LoggerConfiguration MinimalConfiguration(
+ this LoggerConfiguration logConfig,
+ IHostEnvironment hostEnvironment,
+ ILoggingConfiguration loggingConfiguration,
+ UmbracoFileConfiguration umbracoFileConfiguration)
+ {
+ 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", hostEnvironment.MapPathContentRoot("/").TrimEnd("\\"), EnvironmentVariableTarget.Process);
+ Environment.SetEnvironmentVariable("UMBLOGDIR", loggingConfiguration.LogDirectory, 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("ApplicationId", hostEnvironment.GetTemporaryApplicationId()) // Updated later by ApplicationIdEnricher
+ .Enrich.WithProperty("MachineName", Environment.MachineName)
+ .Enrich.With()
+ .Enrich.FromLogContext(); // allows us to dynamically enrich
+
+ logConfig.WriteTo.UmbracoFile(
+ path: umbracoFileConfiguration.GetPath(loggingConfiguration.LogDirectory),
+ fileSizeLimitBytes: umbracoFileConfiguration.FileSizeLimitBytes,
+ restrictedToMinimumLevel: umbracoFileConfiguration.RestrictedToMinimumLevel,
+ rollingInterval: umbracoFileConfiguration.RollingInterval,
+ flushToDiskInterval: umbracoFileConfiguration.FlushToDiskInterval,
+ rollOnFileSizeLimit: umbracoFileConfiguration.RollOnFileSizeLimit,
+ retainedFileCountLimit: umbracoFileConfiguration.RetainedFileCountLimit
+ );
+
+ return logConfig;
+ }
+
///
/// Outputs a .txt format log at /App_Data/Logs/
///
@@ -90,7 +132,7 @@ namespace Umbraco.Extensions
/// 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,
- IHostingEnvironment hostingEnvironment,
+ Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment,
LogEventLevel minimumLevel = LogEventLevel.Verbose)
{
//Main .txt logfile - in similar format to older Log4Net output
@@ -109,7 +151,8 @@ namespace Umbraco.Extensions
///
/// Used in config - If renamed or moved to other assembly the config file also has be updated.
///
- public static LoggerConfiguration UmbracoFile(this LoggerSinkConfiguration configuration,
+ public static LoggerConfiguration UmbracoFile(
+ this LoggerSinkConfiguration configuration,
string path,
ITextFormatter? formatter = null,
LogEventLevel restrictedToMinimumLevel = LogEventLevel.Verbose,
@@ -122,30 +165,29 @@ namespace Umbraco.Extensions
Encoding? encoding = null
)
{
+ formatter ??= new CompactJsonFormatter();
- if (formatter is null)
- {
- formatter = new CompactJsonFormatter();
- }
-
+ /* Async sink has an event buffer of 10k events (by default) so we're not constantly thrashing the disk.
+ * I noticed that with File buffered + large number of log entries (global minimum Debug)
+ * an ungraceful shutdown would consistently result in output that just stops halfway through an entry.
+ * with buffered false on the inner sink ungraceful shutdowns still don't seem to wreck the file.
+ */
return configuration.Async(
- asyncConfiguration => asyncConfiguration.Map(AppDomainId, (_,mapConfiguration) =>
- mapConfiguration.File(
- formatter,
- path,
- restrictedToMinimumLevel,
- fileSizeLimitBytes,
- levelSwitch,
- buffered:true,
- shared:false,
- flushToDiskInterval,
- rollingInterval,
- rollOnFileSizeLimit,
- retainedFileCountLimit,
- encoding,
- null),
- sinkMapCountLimit:0)
- );
+ cfg =>
+ cfg.File(
+ formatter,
+ path,
+ restrictedToMinimumLevel,
+ fileSizeLimitBytes,
+ levelSwitch,
+ buffered: false, // see notes above.
+ shared: false,
+ flushToDiskInterval,
+ rollingInterval,
+ rollOnFileSizeLimit,
+ retainedFileCountLimit,
+ encoding,
+ null));
}
@@ -158,7 +200,7 @@ namespace Umbraco.Extensions
/// 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,
- IHostingEnvironment hostingEnvironment,
+ Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment,
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)
diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs
index 151f3e760c..2ca43efd0c 100644
--- a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs
+++ b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs
@@ -1,8 +1,8 @@
using System;
using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
using Serilog;
using Serilog.Events;
-using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Infrastructure.Logging.Serilog;
using Umbraco.Extensions;
@@ -21,8 +21,9 @@ namespace Umbraco.Cms.Core.Logging.Serilog
SerilogLog = logConfig.CreateLogger();
}
+ [Obsolete]
public static SerilogLogger CreateWithDefaultConfiguration(
- IHostingEnvironment hostingEnvironment,
+ Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment,
ILoggingConfiguration loggingConfiguration,
IConfiguration configuration)
{
@@ -33,8 +34,9 @@ namespace Umbraco.Cms.Core.Logging.Serilog
/// Creates a logger with some pre-defined configuration and remainder from config file
///
/// Used by UmbracoApplicationBase to get its logger.
+ [Obsolete]
public static SerilogLogger CreateWithDefaultConfiguration(
- IHostingEnvironment hostingEnvironment,
+ Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment,
ILoggingConfiguration loggingConfiguration,
IConfiguration configuration,
out UmbracoFileConfiguration umbracoFileConfig)
diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs
index 8889cddebc..ad1622a27e 100644
--- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs
+++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs
@@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
+using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
@@ -34,8 +35,8 @@ namespace Umbraco.Cms.Infrastructure.Runtime
private readonly IEventAggregator _eventAggregator;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IUmbracoVersion _umbracoVersion;
- private readonly IServiceProvider? _serviceProvider;
- private readonly IHostApplicationLifetime? _hostApplicationLifetime;
+ private readonly IServiceProvider _serviceProvider;
+ private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly ILogger _logger;
private CancellationToken _cancellationToken;
@@ -53,8 +54,8 @@ namespace Umbraco.Cms.Infrastructure.Runtime
IEventAggregator eventAggregator,
IHostingEnvironment hostingEnvironment,
IUmbracoVersion umbracoVersion,
- IServiceProvider? serviceProvider,
- IHostApplicationLifetime? hostApplicationLifetime)
+ IServiceProvider serviceProvider,
+ IHostApplicationLifetime hostApplicationLifetime)
{
State = state;
@@ -85,7 +86,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime
IEventAggregator eventAggregator,
IHostingEnvironment hostingEnvironment,
IUmbracoVersion umbracoVersion,
- IServiceProvider? serviceProvider)
+ IServiceProvider serviceProvider)
: this(
state,
loggerFactory,
@@ -153,14 +154,14 @@ namespace Umbraco.Cms.Infrastructure.Runtime
// Store token, so we can re-use this during restart
_cancellationToken = cancellationToken;
+ // Just in-case HostBuilder.ConfigureUmbracoDefaults() isn't used (e.g. upgrade from 9 and ignored advice).
+ if (StaticServiceProvider.Instance == null!)
+ {
+ StaticServiceProvider.Instance = _serviceProvider;
+ }
+
if (isRestarting == false)
{
- StaticApplicationLogging.Initialize(_loggerFactory);
- if (_serviceProvider is not null)
- {
- StaticServiceProvider.Instance = _serviceProvider;
- }
-
AppDomain.CurrentDomain.UnhandledException += (_, args)
=> _logger.LogError(args.ExceptionObject as Exception, $"Unhandled exception in AppDomain{(args.IsTerminating ? " (terminating)" : null)}.");
}
@@ -220,8 +221,8 @@ namespace Umbraco.Cms.Infrastructure.Runtime
if (isRestarting == false)
{
// Add application started and stopped notifications last (to ensure they're always published after starting)
- _hostApplicationLifetime?.ApplicationStarted.Register(() => _eventAggregator.Publish(new UmbracoApplicationStartedNotification(false)));
- _hostApplicationLifetime?.ApplicationStopped.Register(() => _eventAggregator.Publish(new UmbracoApplicationStoppedNotification(false)));
+ _hostApplicationLifetime.ApplicationStarted.Register(() => _eventAggregator.Publish(new UmbracoApplicationStartedNotification(false)));
+ _hostApplicationLifetime.ApplicationStopped.Register(() => _eventAggregator.Publish(new UmbracoApplicationStoppedNotification(false)));
}
}
@@ -233,7 +234,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime
private void AcquireMainDom()
{
- using DisposableTimer? timer = _profilingLogger.DebugDuration("Acquiring MainDom.", "Acquired.");
+ using DisposableTimer timer = _profilingLogger.DebugDuration("Acquiring MainDom.", "Acquired.");
try
{
@@ -254,7 +255,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime
return;
}
- using DisposableTimer? timer = _profilingLogger.DebugDuration("Determining runtime level.", "Determined.");
+ using DisposableTimer timer = _profilingLogger.DebugDuration("Determining runtime level.", "Determined.");
try
{
diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs
index d672f2ae1c..f73fb1b0ae 100644
--- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs
+++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs
@@ -1,14 +1,15 @@
using System;
-using System.Collections.Generic;
using System.IO;
-using System.Threading;
-using Microsoft.AspNetCore.DataProtection;
+using Microsoft.AspNetCore.DataProtection.Infrastructure;
using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Collections;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Models.PublishedContent;
+using Umbraco.Cms.Web.Common.Extensions;
using Umbraco.Extensions;
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
@@ -17,21 +18,39 @@ namespace Umbraco.Cms.Web.Common.AspNetCore
public class AspNetCoreHostingEnvironment : IHostingEnvironment
{
private readonly ConcurrentHashSet _applicationUrls = new ConcurrentHashSet();
- private readonly IServiceProvider _serviceProvider;
private readonly IOptionsMonitor _hostingSettings;
private readonly IOptionsMonitor _webRoutingSettings;
private readonly IWebHostEnvironment _webHostEnvironment;
+ private readonly IApplicationDiscriminator? _applicationDiscriminator;
+
private string? _applicationId;
private string? _localTempPath;
+
private UrlMode _urlProviderMode;
+ [Obsolete("Please use an alternative constructor.")]
public AspNetCoreHostingEnvironment(
IServiceProvider serviceProvider,
IOptionsMonitor hostingSettings,
IOptionsMonitor webRoutingSettings,
IWebHostEnvironment webHostEnvironment)
+ : this(hostingSettings, webRoutingSettings, webHostEnvironment, serviceProvider.GetService())
+ {
+ }
+
+ public AspNetCoreHostingEnvironment(
+ IOptionsMonitor hostingSettings,
+ IOptionsMonitor webRoutingSettings,
+ IWebHostEnvironment webHostEnvironment,
+ IApplicationDiscriminator applicationDiscriminator)
+ : this(hostingSettings, webRoutingSettings, webHostEnvironment) =>
+ _applicationDiscriminator = applicationDiscriminator;
+
+ public AspNetCoreHostingEnvironment(
+ IOptionsMonitor hostingSettings,
+ IOptionsMonitor webRoutingSettings,
+ IWebHostEnvironment webHostEnvironment)
{
- _serviceProvider = serviceProvider;
_hostingSettings = hostingSettings ?? throw new ArgumentNullException(nameof(hostingSettings));
_webRoutingSettings = webRoutingSettings ?? throw new ArgumentNullException(nameof(webRoutingSettings));
_webHostEnvironment = webHostEnvironment ?? throw new ArgumentNullException(nameof(webHostEnvironment));
@@ -74,16 +93,7 @@ namespace Umbraco.Cms.Web.Common.AspNetCore
return _applicationId;
}
- var appId = _serviceProvider.GetApplicationUniqueIdentifier();
- if (appId == null)
- {
- throw new InvalidOperationException("Could not acquire an ApplicationId, ensure DataProtection services and an IHostEnvironment are registered");
- }
-
- // Hash this value because it can really be anything. By default this will be the application's path.
- // TODO: Test on IIS, hopefully this would be equivalent to the IIS unique ID.
- // This could also contain sensitive information (i.e. like the physical path) which we don't want to expose in logs.
- _applicationId = appId.GenerateHash();
+ _applicationId = _applicationDiscriminator?.GetApplicationId() ?? _webHostEnvironment.GetTemporaryApplicationId();
return _applicationId;
}
@@ -136,27 +146,10 @@ namespace Umbraco.Cms.Web.Common.AspNetCore
}
///
- public string MapPathWebRoot(string path) => MapPath(_webHostEnvironment.WebRootPath, path);
+ public string MapPathWebRoot(string path) => _webHostEnvironment.MapPathWebRoot(path);
///
- public string MapPathContentRoot(string path) => MapPath(_webHostEnvironment.ContentRootPath, path);
-
- private string MapPath(string root, string path)
- {
- var newPath = path.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar);
-
- // TODO: This is a temporary error because we switched from IOHelper.MapPath to HostingEnvironment.MapPathXXX
- // IOHelper would check if the path passed in started with the root, and not prepend the root again if it did,
- // however if you are requesting a path be mapped, it should always assume the path is relative to the root, not
- // absolute in the file system. This error will help us find and fix improper uses, and should be removed once
- // all those uses have been found and fixed
- if (newPath.StartsWith(root))
- {
- throw new ArgumentException("The path appears to already be fully qualified. Please remove the call to MapPath");
- }
-
- return Path.Combine(root, newPath.TrimStart(Core.Constants.CharArrays.TildeForwardSlashBackSlash));
- }
+ public string MapPathContentRoot(string path) => _webHostEnvironment.MapPathContentRoot(path);
///
public string ToAbsolute(string virtualPath)
diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs
index 83144a7466..acf3e903b9 100644
--- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs
+++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs
@@ -1,8 +1,8 @@
-using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using SixLabors.ImageSharp.Web.Caching;
@@ -12,6 +12,7 @@ using SixLabors.ImageSharp.Web.Middleware;
using SixLabors.ImageSharp.Web.Processors;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
+using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Cms.Web.Common.ImageProcessors;
@@ -73,9 +74,13 @@ namespace Umbraco.Extensions
return Task.CompletedTask;
};
- })
- .Configure(options => options.CacheFolder = builder.BuilderHostingEnvironment?.MapPathContentRoot(imagingSettings.Cache.CacheFolder))
- .AddProcessor();
+ }).AddProcessor();
+
+ builder.Services.AddOptions()
+ .Configure((opt, hostEnvironment) =>
+ {
+ opt.CacheFolder = hostEnvironment.MapPathContentRoot(imagingSettings.Cache.CacheFolder);
+ });
// Configure middleware to use the registered/shared ImageSharp configuration
builder.Services.AddTransient, ImageSharpConfigurationOptions>();
diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs
index c583ead527..4bc2a49525 100644
--- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs
+++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs
@@ -1,23 +1,23 @@
using System;
using System.Data.Common;
-using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using Dazinator.Extensions.FileProviders.GlobPatternFilter;
using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.DataProtection.Infrastructure;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Server.Kestrel.Core;
-using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
-using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Serilog;
+using Serilog.Extensions.Hosting;
+using Serilog.Extensions.Logging;
using Smidge;
using Smidge.Cache;
using Smidge.FileProcessors;
@@ -29,6 +29,7 @@ using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Diagnostics;
+using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Logging;
using Umbraco.Cms.Core.Macros;
@@ -90,15 +91,12 @@ namespace Umbraco.Extensions
throw new ArgumentNullException(nameof(config));
}
- IHostingEnvironment tempHostingEnvironment = GetTemporaryHostingEnvironment(webHostEnvironment, config);
-
- var loggingDir = tempHostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.LogFiles);
- var loggingConfig = new LoggingConfiguration(loggingDir);
-
- services.AddLogger(tempHostingEnvironment, loggingConfig, config);
+ // Setup static application logging ASAP (e.g. during configure services).
+ // Will log to SilentLogger until Serilog.Log.Logger is setup.
+ StaticApplicationLogging.Initialize(new SerilogLoggerFactory());
// The DataDirectory is used to resolve database file paths (directly supported by SQL CE and manually replaced for LocalDB)
- AppDomain.CurrentDomain.SetData("DataDirectory", tempHostingEnvironment?.MapPathContentRoot(Constants.SystemDirectories.Data));
+ AppDomain.CurrentDomain.SetData("DataDirectory", webHostEnvironment?.MapPathContentRoot(Constants.SystemDirectories.Data));
// Manually create and register the HttpContextAccessor. In theory this should not be registered
// again by the user but if that is the case it's not the end of the world since HttpContextAccessor
@@ -114,19 +112,17 @@ namespace Umbraco.Extensions
IProfiler profiler = GetWebProfiler(config);
- ILoggerFactory loggerFactory = LoggerFactory.Create(cfg => cfg.AddSerilog(Log.Logger, false));
+ services.AddLogger(webHostEnvironment, config);
- TypeLoader typeLoader = services.AddTypeLoader(
- Assembly.GetEntryAssembly(),
- tempHostingEnvironment,
- loggerFactory,
- appCaches,
- config,
- profiler);
+ ILoggerFactory loggerFactory = new SerilogLoggerFactory();
+ TypeLoader typeLoader = services.AddTypeLoader(Assembly.GetEntryAssembly(), loggerFactory, config);
+
+ IHostingEnvironment tempHostingEnvironment = GetTemporaryHostingEnvironment(webHostEnvironment, config);
return new UmbracoBuilder(services, config, typeLoader, loggerFactory, profiler, appCaches, tempHostingEnvironment);
}
+
///
/// Adds core Umbraco services
///
@@ -142,7 +138,8 @@ namespace Umbraco.Extensions
// Add ASP.NET specific services
builder.Services.AddUnique();
- builder.Services.AddUnique();
+ builder.Services.AddUnique(sp => ActivatorUtilities.CreateInstance(sp, sp.GetRequiredService()));
+
builder.Services.AddHostedService(factory => factory.GetRequiredService());
builder.Services.AddSingleton();
@@ -413,22 +410,15 @@ namespace Umbraco.Extensions
private static IHostingEnvironment GetTemporaryHostingEnvironment(IWebHostEnvironment webHostEnvironment, IConfiguration config)
{
var hostingSettings = config.GetSection(Cms.Core.Constants.Configuration.ConfigHosting).Get() ?? new HostingSettings();
- var webRoutingSettings = config.GetSection(Cms.Core.Constants.Configuration.ConfigWebRouting).Get() ?? new WebRoutingSettings();
var wrappedHostingSettings = new OptionsMonitorAdapter(hostingSettings);
+
+ var webRoutingSettings = config.GetSection(Cms.Core.Constants.Configuration.ConfigWebRouting).Get() ?? new WebRoutingSettings();
var wrappedWebRoutingSettings = new OptionsMonitorAdapter(webRoutingSettings);
- // This is needed in order to create a unique Application Id
- var serviceCollection = new ServiceCollection();
- serviceCollection.AddDataProtection();
- serviceCollection.AddSingleton(s => webHostEnvironment);
- var serviceProvider = serviceCollection.BuildServiceProvider();
-
return new AspNetCoreHostingEnvironment(
- serviceProvider,
wrappedHostingSettings,
wrappedWebRoutingSettings,
webHostEnvironment);
}
-
}
}
diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationDiscriminatorExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationDiscriminatorExtensions.cs
new file mode 100644
index 0000000000..398cb40980
--- /dev/null
+++ b/src/Umbraco.Web.Common/Extensions/ApplicationDiscriminatorExtensions.cs
@@ -0,0 +1,35 @@
+using System;
+using Microsoft.AspNetCore.DataProtection.Infrastructure;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Web.Common.Extensions
+{
+ ///
+ /// Contains extension methods for the interface.
+ ///
+ public static class ApplicationDiscriminatorExtensions
+ {
+ private static string s_applicationId;
+
+ ///
+ /// Gets an application id which respects downstream customizations.
+ ///
+ ///
+ /// Hashed to obscure any unintended infrastructure details e.g. the default value is ContentRootPath.
+ ///
+ public static string GetApplicationId(this IApplicationDiscriminator applicationDiscriminator)
+ {
+ if (s_applicationId != null)
+ {
+ return s_applicationId;
+ }
+
+ if (applicationDiscriminator == null)
+ {
+ throw new ArgumentNullException(nameof(applicationDiscriminator));
+ }
+
+ return s_applicationId = applicationDiscriminator.Discriminator.GenerateHash();
+ }
+ }
+}
diff --git a/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs
index b8de39bb65..f5a57161d4 100644
--- a/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs
+++ b/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs
@@ -1,20 +1,28 @@
using System;
-using System.IO;
using System.Reflection;
-using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Serilog;
using Serilog.Extensions.Hosting;
+using Serilog.Extensions.Logging;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.DependencyInjection;
+using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Logging;
using Umbraco.Cms.Core.Logging.Serilog;
+using Umbraco.Cms.Web.Common.Hosting;
+using Umbraco.Cms.Infrastructure.Logging.Serilog;
+using Umbraco.Cms.Web.Common.Logging.Enrichers;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Web.Common.Logging;
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
+using ILogger = Serilog.ILogger;
namespace Umbraco.Extensions
{
@@ -23,6 +31,7 @@ namespace Umbraco.Extensions
///
/// Create and configure the logger
///
+ [Obsolete("Use the extension method that takes an IHostEnvironment instance instead.")]
public static IServiceCollection AddLogger(
this IServiceCollection services,
IHostingEnvironment hostingEnvironment,
@@ -63,17 +72,103 @@ namespace Umbraco.Extensions
return services;
}
+ ///
+ /// Create and configure the logger.
+ ///
+ ///
+ /// Additional Serilog services are registered during .
+ ///
+ public static IServiceCollection AddLogger(
+ this IServiceCollection services,
+ IHostEnvironment hostEnvironment,
+ IConfiguration configuration)
+ {
+ // TODO: WEBSITE_RUN_FROM_PACKAGE - can't assume this DIR is writable - we have an IConfiguration instance so a later refactor should be easy enough.
+ var loggingDir = hostEnvironment.MapPathContentRoot(Constants.SystemDirectories.LogFiles);
+ ILoggingConfiguration loggingConfig = new LoggingConfiguration(loggingDir);
+
+ var umbracoFileConfiguration = new UmbracoFileConfiguration(configuration);
+
+ services.TryAddSingleton(umbracoFileConfiguration);
+ services.TryAddSingleton(loggingConfig);
+ services.TryAddSingleton();
+
+ ///////////////////////////////////////////////
+ // Bootstrap logger setup
+ ///////////////////////////////////////////////
+
+ LoggerConfiguration serilogConfig = new LoggerConfiguration()
+ .MinimalConfiguration(hostEnvironment, loggingConfig, umbracoFileConfiguration)
+ .ReadFrom.Configuration(configuration);
+
+ Log.Logger = serilogConfig.CreateBootstrapLogger();
+
+ ///////////////////////////////////////////////
+ // Runtime logger setup
+ ///////////////////////////////////////////////
+
+ services.AddSingleton(sp =>
+ {
+ var logger = new RegisteredReloadableLogger(Log.Logger as ReloadableLogger);
+
+ logger.Reload(cfg =>
+ {
+ cfg.MinimalConfiguration(hostEnvironment, loggingConfig, umbracoFileConfiguration)
+ .ReadFrom.Configuration(configuration)
+ .ReadFrom.Services(sp);
+
+ return cfg;
+ });
+
+ return logger;
+ });
+
+ services.AddSingleton(sp =>
+ {
+ ILogger logger = sp.GetRequiredService().Logger;
+ return logger.ForContext(new NoopEnricher());
+ });
+
+ services.AddSingleton(sp =>
+ {
+ ILogger logger = sp.GetRequiredService().Logger;
+ return new SerilogLoggerFactory(logger, false);
+ });
+
+ // Registered to provide two services...
+ var diagnosticContext = new DiagnosticContext(Log.Logger);
+
+ // Consumed by e.g. middleware
+ services.TryAddSingleton(diagnosticContext);
+
+ // Consumed by user code
+ services.TryAddSingleton(diagnosticContext);
+
+ return services;
+ }
+
+ ///
+ /// Called to create the to assign to the
+ ///
+ ///
+ /// This should never be called in a web project. It is used internally by Umbraco but could be used in unit tests.
+ /// If called in a web project it will have no affect except to create and return a new TypeLoader but this will not
+ /// be the instance in DI.
+ ///
+ [Obsolete("Please use alternative extension method.")]
+ public static TypeLoader AddTypeLoader(
+ this IServiceCollection services,
+ Assembly entryAssembly,
+ IHostingEnvironment hostingEnvironment,
+ ILoggerFactory loggerFactory,
+ AppCaches appCaches,
+ IConfiguration configuration,
+ IProfiler profiler) =>
+ services.AddTypeLoader(entryAssembly, loggerFactory, configuration);
+
///
/// Called to create the to assign to the
///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
///
/// This should never be called in a web project. It is used internally by Umbraco but could be used in unit tests.
/// If called in a web project it will have no affect except to create and return a new TypeLoader but this will not
@@ -81,12 +176,9 @@ namespace Umbraco.Extensions
///
public static TypeLoader AddTypeLoader(
this IServiceCollection services,
- Assembly? entryAssembly,
- IHostingEnvironment? hostingEnvironment,
+ Assembly entryAssembly,
ILoggerFactory loggerFactory,
- AppCaches appCaches,
- IConfiguration configuration,
- IProfiler profiler)
+ IConfiguration configuration)
{
TypeFinderSettings typeFinderSettings = configuration.GetSection(Cms.Core.Constants.Configuration.ConfigTypeFinder).Get() ?? new TypeFinderSettings();
@@ -95,30 +187,14 @@ namespace Umbraco.Extensions
loggerFactory,
typeFinderSettings.AdditionalEntryAssemblies);
- RuntimeHashPaths runtimeHashPaths = new RuntimeHashPaths().AddAssemblies(assemblyProvider);
-
- var runtimeHash = new RuntimeHash(
- new ProfilingLogger(
- loggerFactory.CreateLogger(),
- profiler),
- runtimeHashPaths);
-
var typeFinderConfig = new TypeFinderConfig(Options.Create(typeFinderSettings));
var typeFinder = new TypeFinder(
loggerFactory.CreateLogger(),
assemblyProvider,
- typeFinderConfig
- );
+ typeFinderConfig);
- var typeLoader = new TypeLoader(
- typeFinder,
- runtimeHash,
- appCaches.RuntimeCache,
- new DirectoryInfo(hostingEnvironment?.LocalTempPath ?? string.Empty),
- loggerFactory.CreateLogger(),
- profiler
- );
+ var typeLoader = new TypeLoader(typeFinder, loggerFactory.CreateLogger());
// This will add it ONCE and not again which is what we want since we don't actually want people to call this method
// in the web project.
diff --git a/src/Umbraco.Web.Common/Extensions/WebHostEnvironmentExtensions.cs b/src/Umbraco.Web.Common/Extensions/WebHostEnvironmentExtensions.cs
new file mode 100644
index 0000000000..78587db1fe
--- /dev/null
+++ b/src/Umbraco.Web.Common/Extensions/WebHostEnvironmentExtensions.cs
@@ -0,0 +1,38 @@
+using System;
+using System.IO;
+using Microsoft.AspNetCore.Hosting;
+
+namespace Umbraco.Cms.Web.Common.Extensions
+{
+ ///
+ /// Contains extension methods for the interface.
+ ///
+ public static class WebHostEnvironmentExtensions
+ {
+ ///
+ /// 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 /wwwroot therefore this will Map to a physical path within wwwroot.
+ ///
+ public static string MapPathWebRoot(this IWebHostEnvironment webHostEnvironment, string path)
+ {
+ var root = webHostEnvironment.WebRootPath;
+
+ var newPath = path.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar);
+
+ // TODO: This is a temporary error because we switched from IOHelper.MapPath to HostingEnvironment.MapPathXXX
+ // IOHelper would check if the path passed in started with the root, and not prepend the root again if it did,
+ // however if you are requesting a path be mapped, it should always assume the path is relative to the root, not
+ // absolute in the file system. This error will help us find and fix improper uses, and should be removed once
+ // all those uses have been found and fixed
+ if (newPath.StartsWith(root))
+ {
+ throw new ArgumentException("The path appears to already be fully qualified. Please remove the call to MapPathWebRoot");
+ }
+
+ return Path.Combine(root, newPath.TrimStart(Core.Constants.CharArrays.TildeForwardSlashBackSlash));
+ }
+ }
+}
diff --git a/src/Umbraco.Web.Common/Hosting/HostBuilderExtensions.cs b/src/Umbraco.Web.Common/Hosting/HostBuilderExtensions.cs
new file mode 100644
index 0000000000..59f5bac85a
--- /dev/null
+++ b/src/Umbraco.Web.Common/Hosting/HostBuilderExtensions.cs
@@ -0,0 +1,34 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Umbraco.Cms.Web.Common.DependencyInjection;
+
+namespace Umbraco.Cms.Web.Common.Hosting;
+
+///
+/// Umbraco specific extensions for the interface.
+///
+public static class HostBuilderExtensions
+{
+ ///
+ /// Configures an existing with defaults for an Umbraco application.
+ ///
+ public static IHostBuilder ConfigureUmbracoDefaults(this IHostBuilder builder)
+ {
+#if DEBUG
+ builder.ConfigureAppConfiguration(config
+ => config.AddJsonFile(
+ "appsettings.Local.json",
+ optional: true,
+ reloadOnChange: true));
+
+#endif
+ builder.ConfigureLogging(x => x.ClearProviders());
+
+ return new UmbracoHostBuilderDecorator(builder, OnHostBuilt);
+ }
+
+ // Runs before any IHostedService starts (including generic web host).
+ private static void OnHostBuilt(IHost host) =>
+ StaticServiceProvider.Instance = host.Services;
+}
diff --git a/src/Umbraco.Web.Common/Hosting/UmbracoHostBuilderDecorator.cs b/src/Umbraco.Web.Common/Hosting/UmbracoHostBuilderDecorator.cs
new file mode 100644
index 0000000000..b2f6fc112c
--- /dev/null
+++ b/src/Umbraco.Web.Common/Hosting/UmbracoHostBuilderDecorator.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace Umbraco.Cms.Web.Common.Hosting;
+
+internal class UmbracoHostBuilderDecorator : IHostBuilder
+{
+ private readonly IHostBuilder _inner;
+ private readonly Action _onBuild;
+
+ public UmbracoHostBuilderDecorator(IHostBuilder inner, Action onBuild = null)
+ {
+ _inner = inner;
+ _onBuild = onBuild;
+ }
+
+ public IHostBuilder ConfigureAppConfiguration(Action configureDelegate) =>
+ _inner.ConfigureAppConfiguration(configureDelegate);
+
+ public IHostBuilder ConfigureContainer(Action configureDelegate) =>
+ _inner.ConfigureContainer(configureDelegate);
+
+ public IHostBuilder ConfigureHostConfiguration(Action configureDelegate) =>
+ _inner.ConfigureHostConfiguration(configureDelegate);
+
+ public IHostBuilder ConfigureServices(Action configureDelegate) =>
+ _inner.ConfigureServices(configureDelegate);
+
+ public IHostBuilder UseServiceProviderFactory(IServiceProviderFactory factory) =>
+ _inner.UseServiceProviderFactory(factory);
+
+ public IHostBuilder UseServiceProviderFactory(Func> factory) =>
+ _inner.UseServiceProviderFactory(factory);
+
+ public IDictionary