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 Properties => _inner.Properties; + + public IHost Build() + { + IHost host = _inner.Build(); + + _onBuild?.Invoke(host); + + return host; + } +} diff --git a/src/Umbraco.Web.Common/Logging/Enrichers/ApplicationIdEnricher.cs b/src/Umbraco.Web.Common/Logging/Enrichers/ApplicationIdEnricher.cs new file mode 100644 index 0000000000..b314745fae --- /dev/null +++ b/src/Umbraco.Web.Common/Logging/Enrichers/ApplicationIdEnricher.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.DataProtection.Infrastructure; +using Serilog.Core; +using Serilog.Events; +using Umbraco.Cms.Web.Common.Extensions; + +namespace Umbraco.Cms.Web.Common.Logging.Enrichers; + +internal class ApplicationIdEnricher : ILogEventEnricher +{ + private readonly IApplicationDiscriminator _applicationDiscriminator; + public const string ApplicationIdProperty = "ApplicationId"; + + public ApplicationIdEnricher(IApplicationDiscriminator applicationDiscriminator) => + _applicationDiscriminator = applicationDiscriminator; + + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) => + logEvent.AddOrUpdateProperty(propertyFactory.CreateProperty(ApplicationIdProperty, _applicationDiscriminator.GetApplicationId())); +} diff --git a/src/Umbraco.Web.Common/Logging/Enrichers/NoopEnricher.cs b/src/Umbraco.Web.Common/Logging/Enrichers/NoopEnricher.cs new file mode 100644 index 0000000000..86402afd45 --- /dev/null +++ b/src/Umbraco.Web.Common/Logging/Enrichers/NoopEnricher.cs @@ -0,0 +1,14 @@ +using Serilog.Core; +using Serilog.Events; + +namespace Umbraco.Cms.Web.Common.Logging.Enrichers; + +/// +/// NoOp but useful for tricks to avoid disposal of the global logger. +/// +internal class NoopEnricher : ILogEventEnricher +{ + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + } +} diff --git a/src/Umbraco.Web.Common/Logging/RegisteredReloadableLogger.cs b/src/Umbraco.Web.Common/Logging/RegisteredReloadableLogger.cs new file mode 100644 index 0000000000..4bc8ac1dbc --- /dev/null +++ b/src/Umbraco.Web.Common/Logging/RegisteredReloadableLogger.cs @@ -0,0 +1,40 @@ +using System; +using Serilog; +using Serilog.Extensions.Hosting; + +namespace Umbraco.Cms.Web.Common.Logging; + +/// +/// HACK: +/// Ensures freeze is only called a single time even when resolving a logger from the snapshot container +/// built for . +/// +internal class RegisteredReloadableLogger +{ + private static bool s_frozen; + private static object s_frozenLock = new(); + private readonly ReloadableLogger _logger; + + public RegisteredReloadableLogger(ReloadableLogger logger) => + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + public ILogger Logger => _logger; + + public void Reload(Func cfg) + { + lock (s_frozenLock) + { + if (s_frozen) + { + Logger.Debug("ReloadableLogger has already been frozen, unable to reload, NOOP."); + return; + } + + _logger.Reload(cfg); + _logger.Freeze(); + + s_frozen = true; + } + } +} + diff --git a/src/Umbraco.Web.UI/Program.cs b/src/Umbraco.Web.UI/Program.cs index 9b77c126b7..ee0a4774be 100644 --- a/src/Umbraco.Web.UI/Program.cs +++ b/src/Umbraco.Web.UI/Program.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; +using Umbraco.Cms.Web.Common.Hosting; namespace Umbraco.Cms.Web.UI { @@ -12,12 +11,9 @@ namespace Umbraco.Cms.Web.UI .Build() .Run(); - public static IHostBuilder CreateHostBuilder(string[] args) - => Host.CreateDefaultBuilder(args) -#if DEBUG - .ConfigureAppConfiguration(config => config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: true)) -#endif - .ConfigureLogging(x => x.ClearProviders()) + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureUmbracoDefaults() .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()); } } diff --git a/tests/Umbraco.Tests.Benchmarks/TypeLoaderBenchmarks.cs b/tests/Umbraco.Tests.Benchmarks/TypeLoaderBenchmarks.cs index d11bd6920d..5c3e1ade75 100644 --- a/tests/Umbraco.Tests.Benchmarks/TypeLoaderBenchmarks.cs +++ b/tests/Umbraco.Tests.Benchmarks/TypeLoaderBenchmarks.cs @@ -23,28 +23,8 @@ namespace Umbraco.Tests.Benchmarks new NullLogger(), new DefaultUmbracoAssemblyProvider(GetType().Assembly, NullLoggerFactory.Instance)); - var cache = new ObjectCacheAppCache(); - _typeLoader1 = new TypeLoader( - typeFinder1, - new VaryingRuntimeHash(), - cache, - null, - new NullLogger(), - new NoopProfiler()); - - // populate the cache - cache.Insert( - _typeLoader1.CacheKey, - GetCache, - TimeSpan.FromDays(1)); - - _typeLoader2 = new TypeLoader( - typeFinder1, - new VaryingRuntimeHash(), - NoAppCache.Instance, - null, - new NullLogger(), - new NoopProfiler()); + _typeLoader1 = new TypeLoader(typeFinder1, NullLogger.Instance); + _typeLoader2 = new TypeLoader(typeFinder1, NullLogger.Instance); } /// diff --git a/tests/Umbraco.Tests.Common/Testing/TestHostingEnvironment.cs b/tests/Umbraco.Tests.Common/Testing/TestHostingEnvironment.cs index e34161a3c2..bd88d80bc0 100644 --- a/tests/Umbraco.Tests.Common/Testing/TestHostingEnvironment.cs +++ b/tests/Umbraco.Tests.Common/Testing/TestHostingEnvironment.cs @@ -15,7 +15,7 @@ namespace Umbraco.Cms.Tests.Common.Testing IOptionsMonitor hostingSettings, IOptionsMonitor webRoutingSettings, IWebHostEnvironment webHostEnvironment) - : base(null, hostingSettings, webRoutingSettings, webHostEnvironment) + : base(hostingSettings, webRoutingSettings, webHostEnvironment) { } diff --git a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index e25438b03b..f1f8b124e8 100644 --- a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -26,6 +26,7 @@ using Umbraco.Cms.Tests.Integration.DependencyInjection; using Umbraco.Cms.Tests.Integration.Testing; using Umbraco.Cms.Web.BackOffice.Controllers; using Umbraco.Cms.Web.Common.Controllers; +using Umbraco.Cms.Web.Common.Hosting; using Umbraco.Cms.Web.Website.Controllers; using Umbraco.Extensions; @@ -136,6 +137,7 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest private IHostBuilder CreateHostBuilder() { IHostBuilder hostBuilder = Host.CreateDefaultBuilder() + .ConfigureUmbracoDefaults() .ConfigureAppConfiguration((context, configBuilder) => { context.HostingEnvironment = TestHelper.GetWebHostEnvironment(); @@ -197,6 +199,8 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest Configuration, TestHelper.Profiler); + services.AddLogger(TestHelper.GetWebHostEnvironment(), Configuration); + var builder = new UmbracoBuilder(services, Configuration, typeLoader, TestHelper.ConsoleLoggerFactory, TestHelper.Profiler, AppCaches.NoCache, hostingEnvironment); builder diff --git a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index b11b7ea074..32a58dd702 100644 --- a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -25,6 +25,7 @@ using Umbraco.Cms.Persistence.SqlServer; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Integration.DependencyInjection; using Umbraco.Cms.Tests.Integration.Extensions; +using Umbraco.Cms.Web.Common.Hosting; using Umbraco.Extensions; namespace Umbraco.Cms.Tests.Integration.Testing @@ -66,6 +67,7 @@ namespace Umbraco.Cms.Tests.Integration.Testing private IHostBuilder CreateHostBuilder() { IHostBuilder hostBuilder = Host.CreateDefaultBuilder() + .ConfigureUmbracoDefaults() // IMPORTANT: We Cannot use UseStartup, there's all sorts of threads about this with testing. Although this can work // if you want to setup your tests this way, it is a bit annoying to do that as the WebApplicationFactory will @@ -106,6 +108,8 @@ namespace Umbraco.Cms.Tests.Integration.Testing // We register this service because we need it for IRuntimeState, if we don't this breaks 900 tests services.AddSingleton(); + services.AddLogger(webHostEnvironment, Configuration); + // Add it! Core.Hosting.IHostingEnvironment hostingEnvironment = TestHelper.GetHostingEnvironment(); TypeLoader typeLoader = services.AddTypeLoader( @@ -117,8 +121,6 @@ namespace Umbraco.Cms.Tests.Integration.Testing TestHelper.Profiler); var builder = new UmbracoBuilder(services, Configuration, typeLoader, TestHelper.ConsoleLoggerFactory, TestHelper.Profiler, AppCaches.NoCache, hostingEnvironment); - builder.Services.AddLogger(hostingEnvironment, TestHelper.GetLoggingConfiguration(), Configuration); - builder.AddConfiguration() .AddUmbracoCore() .AddWebComponents() diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeLoaderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeLoaderTests.cs index c71d89856e..19f04ac869 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeLoaderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeLoaderTests.cs @@ -115,118 +115,6 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Composing //// Debug.WriteLine("TOTAL TIME (2nd round): " + watch.ElapsedMilliseconds); ////} - //////NOTE: This test shows that Type.GetType is 100% faster than Assembly.Load(..).GetType(...) so we'll use that :) - ////[Test] - ////public void Load_Type_Benchmark() - ////{ - //// var watch = new Stopwatch(); - //// watch.Start(); - //// for (var i = 0; i < 1000; i++) - //// { - //// var type2 = Type.GetType("umbraco.macroCacheRefresh, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null"); - //// var type3 = Type.GetType("umbraco.templateCacheRefresh, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null"); - //// var type4 = Type.GetType("umbraco.presentation.cache.MediaLibraryRefreshers, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null"); - //// var type5 = Type.GetType("umbraco.presentation.cache.pageRefresher, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null"); - //// } - //// watch.Stop(); - //// Debug.WriteLine("TOTAL TIME (1st round): " + watch.ElapsedMilliseconds); - //// watch.Reset(); - //// watch.Start(); - //// for (var i = 0; i < 1000; i++) - //// { - //// var type2 = Assembly.Load("umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null") - //// .GetType("umbraco.macroCacheRefresh"); - //// var type3 = Assembly.Load("umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null") - //// .GetType("umbraco.templateCacheRefresh"); - //// var type4 = Assembly.Load("umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null") - //// .GetType("umbraco.presentation.cache.MediaLibraryRefreshers"); - //// var type5 = Assembly.Load("umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null") - //// .GetType("umbraco.presentation.cache.pageRefresher"); - //// } - //// watch.Stop(); - //// Debug.WriteLine("TOTAL TIME (2nd round): " + watch.ElapsedMilliseconds); - //// watch.Reset(); - //// watch.Start(); - //// for (var i = 0; i < 1000; i++) - //// { - //// var type2 = BuildManager.GetType("umbraco.macroCacheRefresh, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null", true); - //// var type3 = BuildManager.GetType("umbraco.templateCacheRefresh, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null", true); - //// var type4 = BuildManager.GetType("umbraco.presentation.cache.MediaLibraryRefreshers, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null", true); - //// var type5 = BuildManager.GetType("umbraco.presentation.cache.pageRefresher, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null", true); - //// } - //// watch.Stop(); - //// Debug.WriteLine("TOTAL TIME (1st round): " + watch.ElapsedMilliseconds); - ////} - - [Test] - [Retry(5)] // TODO make this test non-flaky. - public void Detect_Legacy_Plugin_File_List() - { - string filePath = _typeLoader.GetTypesListFilePath(); - string fileDir = Path.GetDirectoryName(filePath); - Directory.CreateDirectory(fileDir); - - File.WriteAllText(filePath, @" - - - - -"); - - Assert.IsEmpty(_typeLoader.ReadCache()); // uber-legacy cannot be read - - File.Delete(filePath); - - File.WriteAllText(filePath, @" - - - - -"); - - Assert.IsEmpty(_typeLoader.ReadCache()); // legacy cannot be read - - File.Delete(filePath); - - File.WriteAllText(filePath, @"IContentFinder - -MyContentFinder -AnotherContentFinder - -"); - - Assert.IsNotNull(_typeLoader.ReadCache()); // works - } - - [Retry(5)] // TODO make this test non-flaky. - [Test] - public void Create_Cached_Plugin_File() - { - Type[] types = new[] { typeof(TypeLoader), typeof(TypeLoaderTests), typeof(IUmbracoContext) }; - - var typeList1 = new TypeLoader.TypeList(typeof(object), null); - foreach (Type type in types) - { - typeList1.Add(type); - } - - _typeLoader.AddTypeList(typeList1); - _typeLoader.WriteCache(); - - Attempt> plugins = _typeLoader.TryGetCached(typeof(object), null); - Attempt> diffType = _typeLoader.TryGetCached(typeof(object), typeof(ObsoleteAttribute)); - - Assert.IsTrue(plugins.Success); - - // This will be false since there is no cache of that type resolution kind - Assert.IsFalse(diffType.Success); - - Assert.AreEqual(3, plugins.Result.Count()); - IEnumerable shouldContain = types.Select(x => x.AssemblyQualifiedName); - - // Ensure they are all found - Assert.IsTrue(plugins.Result.ContainsAll(shouldContain)); - } [Test] public void Get_Plugins_Hash_With_Hash_Generator()