Merge branch 'v10/dev' into v10/contrib

This commit is contained in:
Sebastiaan Janssen
2022-04-26 10:35:36 +02:00
24 changed files with 629 additions and 810 deletions

View File

@@ -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
/// <remarks>
/// <para>This class should be used to get all types, the <see cref="ITypeFinder"/> class should never be used directly.</para>
/// <para>In most cases this class is not used directly but through extension methods that retrieve specific types.</para>
/// <para>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.</para>
/// </remarks>
public sealed class TypeLoader
{
private readonly IRuntimeHash _runtimeHash;
private readonly IAppPolicyCache _runtimeCache;
private readonly ILogger<TypeLoader> _logger;
private readonly IProfilingLogger _profilingLogger;
private readonly Dictionary<CompositeTypeTypeKey, TypeList> _types = new Dictionary<CompositeTypeTypeKey, TypeList>();
private readonly object _locko = new object();
private readonly object _timerLock = new object();
private readonly Dictionary<CompositeTypeTypeKey, TypeList> _types = new ();
private readonly object _locko = new ();
private Timer? _timer;
private bool _timing;
private string? _cachedAssembliesHash;
private string? _currentAssembliesHash;
private IEnumerable<Assembly>? _assemblies;
private bool _reportedChange;
private readonly DirectoryInfo _localTempPath;
private readonly Lazy<string?> _fileBasePath;
private readonly Dictionary<(string, string), IEnumerable<string>> _emptyCache = new Dictionary<(string, string), IEnumerable<string>>();
private string? _typesListFilePath;
private string? _typesHashFilePath;
private IEnumerable<Assembly> _assemblies;
/// <summary>
/// Initializes a new instance of the <see cref="TypeLoader"/> class.
/// </summary>
/// <param name="typeFinder"></param>
/// <param name="runtimeCache">The application runtime cache.</param>
/// <param name="localTempPath">Files storage location.</param>
/// <param name="logger">A profiling logger.</param>
/// <param name="assembliesToScan"></param>
public TypeLoader(ITypeFinder typeFinder, IRuntimeHash runtimeHash, IAppPolicyCache runtimeCache, DirectoryInfo localTempPath, ILogger<TypeLoader> logger, IProfiler profiler, IEnumerable<Assembly>? assembliesToScan = null)
: this(typeFinder, runtimeHash, runtimeCache, localTempPath, logger, profiler, true, assembliesToScan)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="TypeLoader"/> class.
/// </summary>
/// <param name="typeFinder"></param>
/// <param name="runtimeCache">The application runtime cache.</param>
/// <param name="localTempPath">Files storage location.</param>
/// <param name="logger">A profiling logger.</param>
/// <param name="detectChanges">Whether to detect changes using hashes.</param>
/// <param name="assembliesToScan"></param>
public TypeLoader(ITypeFinder typeFinder, IRuntimeHash runtimeHash, IAppPolicyCache runtimeCache, DirectoryInfo localTempPath, ILogger<TypeLoader> logger, IProfiler profiler, bool detectChanges, IEnumerable<Assembly>? assembliesToScan = null)
[Obsolete("Please use an alternative constructor.")]
public TypeLoader(
ITypeFinder typeFinder,
IRuntimeHash runtimeHash,
IAppPolicyCache runtimeCache,
DirectoryInfo localTempPath,
ILogger<TypeLoader> logger,
IProfiler profiler,
IEnumerable<Assembly>? 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<string?>(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; }
/// <summary>
/// Initializes a new instance of the <see cref="TypeLoader"/> class.
/// </summary>
[Obsolete("Please use an alternative constructor.")]
public TypeLoader(
ITypeFinder typeFinder,
IRuntimeHash runtimeHash,
IAppPolicyCache runtimeCache,
DirectoryInfo localTempPath,
ILogger<TypeLoader> logger,
IProfiler profiler,
bool detectChanges,
IEnumerable<Assembly>? assembliesToScan = null)
: this(typeFinder, logger, assembliesToScan)
{
}
public TypeLoader(
ITypeFinder typeFinder,
ILogger<TypeLoader> logger,
IEnumerable<Assembly>? assembliesToScan = null)
{
TypeFinder = typeFinder ?? throw new ArgumentNullException(nameof(typeFinder));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_assemblies = assembliesToScan;
}
/// <summary>
/// Returns the underlying <see cref="ITypeFinder"/>
@@ -144,6 +87,7 @@ namespace Umbraco.Cms.Core.Composing
/// <para>This is for unit tests.</para>
/// </remarks>
// internal for tests
[Obsolete("This will be removed in a future version.")]
public IEnumerable<Assembly> AssembliesToScan => _assemblies ??= TypeFinder.AssembliesToScan;
/// <summary>
@@ -151,6 +95,7 @@ namespace Umbraco.Cms.Core.Composing
/// </summary>
/// <remarks>For unit tests.</remarks>
// internal for tests
[Obsolete("This will be removed in a future version.")]
public IEnumerable<TypeList> TypeLists => _types.Values;
/// <summary>
@@ -158,378 +103,49 @@ namespace Umbraco.Cms.Core.Composing
/// </summary>
/// <remarks>For unit tests.</remarks>
// 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
/// <summary>
/// Gets a value indicating whether the assemblies in bin, app_code, global.asax, etc... have changed since they were last hashed.
/// </summary>
private bool RequiresRescanning { get; }
/// <summary>
/// Gets the currently cached hash value of the scanned assemblies.
/// </summary>
/// <value>The cached hash value, or string.Empty if no cache is found.</value>
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;
}
}
/// <summary>
/// Gets the current assemblies hash based on creating a hash from the assemblies in various places.
/// </summary>
/// <value>The current hash.</value>
private string CurrentAssembliesHash
{
get
{
if (_currentAssembliesHash != null)
{
return _currentAssembliesHash;
}
_currentAssembliesHash = _runtimeHash.GetHashValue();
return _currentAssembliesHash;
}
}
/// <summary>
/// Writes the assembly hash file.
/// </summary>
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<IEnumerable<string>?> TryGetCached(Type? baseType, Type? attributeType)
[Obsolete("This will be removed in a future version.")]
public Attempt<IEnumerable<string>> 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<string>? types);
return types == null
? Attempt<IEnumerable<string>?>.Fail()
: Attempt.Succeed(types);
}
private Dictionary<(string, string), IEnumerable<string>> 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<IEnumerable<string>>.Fail();
}
// internal for tests
public Dictionary<(string, string), IEnumerable<string>> ReadCache()
{
var typesListFilePath = GetTypesListFilePath();
if (typesListFilePath == null || File.Exists(typesListFilePath) == false)
{
return _emptyCache;
}
var cache = new Dictionary<(string, string), IEnumerable<string>>();
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<string>();
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<string>> 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";
/// <summary>
/// Used to produce the Lazy value of _fileBasePath
/// </summary>
/// <returns></returns>
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;
}
}
/// <summary>
/// Removes cache files and internal cache.
/// Clears cache.
/// </summary>
/// <remarks>Generally only used for resetting cache, for example during the install process.</remarks>
[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
///// <summary>
///// Gets the assembly attributes of the specified type <typeparamref name="T" />.
///// </summary>
///// <typeparam name="T">The attribute type.</typeparam>
///// <returns>
///// The assembly attributes of the specified type <typeparamref name="T" />.
///// </returns>
//public IEnumerable<T> GetAssemblyAttributes<T>()
// where T : Attribute
//{
// return AssembliesToScan.SelectMany(a => a.GetCustomAttributes<T>()).ToList();
//}
///// <summary>
///// Gets all the assembly attributes.
///// </summary>
///// <returns>
///// All assembly attributes.
///// </returns>
//public IEnumerable<Attribute> GetAssemblyAttributes()
//{
// return AssembliesToScan.SelectMany(a => a.GetCustomAttributes()).ToList();
//}
/// <summary>
/// Gets the assembly attributes of the specified <paramref name="attributeTypes" />.
/// </summary>
@@ -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<TypeLoader>(
"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
/// </summary>
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);
}

View File

@@ -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
/// <remarks>
/// This may be null.
/// </remarks>
[Obsolete("This property will be removed in a future version, please find an alternative approach.")]
IHostingEnvironment? BuilderHostingEnvironment { get; }
IProfiler Profiler { get; }

View File

@@ -0,0 +1,53 @@
using System;
using System.IO;
using Microsoft.Extensions.Hosting;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Extensions
{
/// <summary>
/// Contains extension methods for the <see cref="IHostEnvironment" /> interface.
/// </summary>
public static class HostEnvironmentExtensions
{
private static string s_temporaryApplicationId;
/// <summary>
/// Maps a virtual path to a physical path to the application's content root.
/// </summary>
/// <remarks>
/// Generally the content root is the parent directory of the web root directory.
/// </remarks>
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));
}
/// <summary>
/// Gets a temporary application id for use before the ioc container is built.
/// </summary>
public static string GetTemporaryApplicationId(this IHostEnvironment hostEnvironment)
{
if (s_temporaryApplicationId != null)
{
return s_temporaryApplicationId;
}
return s_temporaryApplicationId = hostEnvironment.ContentRootPath.GenerateHash();
}
}
}

View File

@@ -15,12 +15,21 @@ namespace Umbraco.Cms.Core.Hosting
/// between restarts of that Umbraco website/application on that specific server.
/// </para>
/// <para>
/// 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 <see cref="ApplicationId"/> 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.<br/>
/// Similarly the usage of this must take into account that a different
/// <see cref="ApplicationId"/> may be returned for the same Umbraco website hosted on different servers.
/// </para>
/// <para>
/// This returns a hash of the value of IApplicationDiscriminator.Discriminator (which is most likely just the value of <see cref=" Microsoft.Extensions.Hosting.IHostEnvironment.ContentRootPath"/> unless an alternative implementation of IApplicationDiscriminator has been registered).<br/>
/// However during ConfigureServices a temporary instance of IHostingEnvironment is constructed which guarantees that this will be the hash of <see cref=" Microsoft.Extensions.Hosting.IHostEnvironment.ContentRootPath"/>, so the value may differ depend on when the property is used.
/// </para>
/// <para>
/// If you require this value during ConfigureServices it is probably a code smell.
/// </para>
/// </remarks>
[Obsolete("Please use IApplicationDiscriminator.Discriminator instead.")]
string ApplicationId { get; }
/// <summary>
@@ -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.
/// </remarks>
[Obsolete("Please use the MapPathWebRoot extension method on an instance of IWebHostEnvironment instead")]
string MapPathWebRoot(string path);
/// <summary>
@@ -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.
/// </remarks>
[Obsolete("Please use the MapPathContentRoot extension method on an instance of IHostEnvironment (or IWebHostEnvironment) instead")]
string MapPathContentRoot(string path);
/// <summary>

View File

@@ -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";
/// <summary>
/// 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
/// </summary>
[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
/// </summary>
[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<Log4NetLevelMapperEnricher>()
.Enrich.FromLogContext(); // allows us to dynamically enrich
@@ -81,6 +81,48 @@ namespace Umbraco.Extensions
return logConfig;
}
/// <summary>
/// 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
/// </summary>
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<Log4NetLevelMapperEnricher>()
.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;
}
/// <summary>
/// Outputs a .txt format log at /App_Data/Logs/
/// </summary>
@@ -90,7 +132,7 @@ namespace Umbraco.Extensions
/// <param name="retainedFileCount">The number of days to keep log files. Default is set to null which means all logs are kept</param>
public static LoggerConfiguration OutputDefaultTextFile(
this LoggerConfiguration logConfig,
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
/// <remarks>
/// Used in config - If renamed or moved to other assembly the config file also has be updated.
/// </remarks>
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
/// <param name="retainedFileCount">The number of days to keep log files. Default is set to null which means all logs are kept</param>
public static LoggerConfiguration OutputDefaultJsonFile(
this LoggerConfiguration logConfig,
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)

View File

@@ -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
/// </summary>
/// <remarks>Used by UmbracoApplicationBase to get its logger.</remarks>
[Obsolete]
public static SerilogLogger CreateWithDefaultConfiguration(
IHostingEnvironment hostingEnvironment,
Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment,
ILoggingConfiguration loggingConfiguration,
IConfiguration configuration,
out UmbracoFileConfiguration umbracoFileConfig)

View File

@@ -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<CoreRuntime> _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<CoreRuntime>("Acquiring MainDom.", "Acquired.");
using DisposableTimer timer = _profilingLogger.DebugDuration<CoreRuntime>("Acquiring MainDom.", "Acquired.");
try
{
@@ -254,7 +255,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime
return;
}
using DisposableTimer? timer = _profilingLogger.DebugDuration<CoreRuntime>("Determining runtime level.", "Determined.");
using DisposableTimer timer = _profilingLogger.DebugDuration<CoreRuntime>("Determining runtime level.", "Determined.");
try
{

View File

@@ -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<Uri> _applicationUrls = new ConcurrentHashSet<Uri>();
private readonly IServiceProvider _serviceProvider;
private readonly IOptionsMonitor<HostingSettings> _hostingSettings;
private readonly IOptionsMonitor<WebRoutingSettings> _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> hostingSettings,
IOptionsMonitor<WebRoutingSettings> webRoutingSettings,
IWebHostEnvironment webHostEnvironment)
: this(hostingSettings, webRoutingSettings, webHostEnvironment, serviceProvider.GetService<IApplicationDiscriminator>())
{
}
public AspNetCoreHostingEnvironment(
IOptionsMonitor<HostingSettings> hostingSettings,
IOptionsMonitor<WebRoutingSettings> webRoutingSettings,
IWebHostEnvironment webHostEnvironment,
IApplicationDiscriminator applicationDiscriminator)
: this(hostingSettings, webRoutingSettings, webHostEnvironment) =>
_applicationDiscriminator = applicationDiscriminator;
public AspNetCoreHostingEnvironment(
IOptionsMonitor<HostingSettings> hostingSettings,
IOptionsMonitor<WebRoutingSettings> 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
}
/// <inheritdoc/>
public string MapPathWebRoot(string path) => MapPath(_webHostEnvironment.WebRootPath, path);
public string MapPathWebRoot(string path) => _webHostEnvironment.MapPathWebRoot(path);
/// <inheritdoc/>
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);
/// <inheritdoc/>
public string ToAbsolute(string virtualPath)

View File

@@ -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<PhysicalFileSystemCacheOptions>(options => options.CacheFolder = builder.BuilderHostingEnvironment?.MapPathContentRoot(imagingSettings.Cache.CacheFolder))
.AddProcessor<CropWebProcessor>();
}).AddProcessor<CropWebProcessor>();
builder.Services.AddOptions<PhysicalFileSystemCacheOptions>()
.Configure<IHostEnvironment>((opt, hostEnvironment) =>
{
opt.CacheFolder = hostEnvironment.MapPathContentRoot(imagingSettings.Cache.CacheFolder);
});
// Configure middleware to use the registered/shared ImageSharp configuration
builder.Services.AddTransient<IConfigureOptions<ImageSharpMiddlewareOptions>, ImageSharpConfigurationOptions>();

View File

@@ -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);
}
/// <summary>
/// Adds core Umbraco services
/// </summary>
@@ -142,7 +138,8 @@ namespace Umbraco.Extensions
// Add ASP.NET specific services
builder.Services.AddUnique<IBackOfficeInfo, AspNetCoreBackOfficeInfo>();
builder.Services.AddUnique<IHostingEnvironment, AspNetCoreHostingEnvironment>();
builder.Services.AddUnique<IHostingEnvironment>(sp => ActivatorUtilities.CreateInstance<AspNetCoreHostingEnvironment>(sp, sp.GetRequiredService<IApplicationDiscriminator>()));
builder.Services.AddHostedService(factory => factory.GetRequiredService<IRuntime>());
builder.Services.AddSingleton<DatabaseSchemaCreatorFactory>();
@@ -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<HostingSettings>() ?? new HostingSettings();
var webRoutingSettings = config.GetSection(Cms.Core.Constants.Configuration.ConfigWebRouting).Get<WebRoutingSettings>() ?? new WebRoutingSettings();
var wrappedHostingSettings = new OptionsMonitorAdapter<HostingSettings>(hostingSettings);
var webRoutingSettings = config.GetSection(Cms.Core.Constants.Configuration.ConfigWebRouting).Get<WebRoutingSettings>() ?? new WebRoutingSettings();
var wrappedWebRoutingSettings = new OptionsMonitorAdapter<WebRoutingSettings>(webRoutingSettings);
// This is needed in order to create a unique Application Id
var serviceCollection = new ServiceCollection();
serviceCollection.AddDataProtection();
serviceCollection.AddSingleton<IHostEnvironment>(s => webHostEnvironment);
var serviceProvider = serviceCollection.BuildServiceProvider();
return new AspNetCoreHostingEnvironment(
serviceProvider,
wrappedHostingSettings,
wrappedWebRoutingSettings,
webHostEnvironment);
}
}
}

View File

@@ -0,0 +1,35 @@
using System;
using Microsoft.AspNetCore.DataProtection.Infrastructure;
using Umbraco.Extensions;
namespace Umbraco.Cms.Web.Common.Extensions
{
/// <summary>
/// Contains extension methods for the <see cref="IApplicationDiscriminator" /> interface.
/// </summary>
public static class ApplicationDiscriminatorExtensions
{
private static string s_applicationId;
/// <summary>
/// Gets an application id which respects downstream customizations.
/// </summary>
/// <remarks>
/// Hashed to obscure any unintended infrastructure details e.g. the default value is ContentRootPath.
/// </remarks>
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();
}
}
}

View File

@@ -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
/// <summary>
/// Create and configure the logger
/// </summary>
[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;
}
/// <summary>
/// Create and configure the logger.
/// </summary>
/// <remarks>
/// Additional Serilog services are registered during <see cref="HostBuilderExtensions.ConfigureUmbracoDefaults"/>.
/// </remarks>
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<Serilog.Core.ILogEventEnricher, ApplicationIdEnricher>();
///////////////////////////////////////////////
// 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<Serilog.ILogger>(sp =>
{
ILogger logger = sp.GetRequiredService<RegisteredReloadableLogger>().Logger;
return logger.ForContext(new NoopEnricher());
});
services.AddSingleton<ILoggerFactory>(sp =>
{
ILogger logger = sp.GetRequiredService<RegisteredReloadableLogger>().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<IDiagnosticContext>(diagnosticContext);
return services;
}
/// <summary>
/// Called to create the <see cref="TypeLoader"/> to assign to the <see cref="IUmbracoBuilder"/>
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[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);
/// <summary>
/// Called to create the <see cref="TypeLoader"/> to assign to the <see cref="IUmbracoBuilder"/>
/// </summary>
/// <param name="services"></param>
/// <param name="entryAssembly"></param>
/// <param name="hostingEnvironment"></param>
/// <param name="loggerFactory"></param>
/// <param name="appCaches"></param>
/// <param name="configuration"></param>
/// <param name="profiler"></param>
/// <returns></returns>
/// <remarks>
/// 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
/// </remarks>
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<TypeFinderSettings>() ?? 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<RuntimeHash>(),
profiler),
runtimeHashPaths);
var typeFinderConfig = new TypeFinderConfig(Options.Create(typeFinderSettings));
var typeFinder = new TypeFinder(
loggerFactory.CreateLogger<TypeFinder>(),
assemblyProvider,
typeFinderConfig
);
typeFinderConfig);
var typeLoader = new TypeLoader(
typeFinder,
runtimeHash,
appCaches.RuntimeCache,
new DirectoryInfo(hostingEnvironment?.LocalTempPath ?? string.Empty),
loggerFactory.CreateLogger<TypeLoader>(),
profiler
);
var typeLoader = new TypeLoader(typeFinder, loggerFactory.CreateLogger<TypeLoader>());
// 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.

View File

@@ -0,0 +1,38 @@
using System;
using System.IO;
using Microsoft.AspNetCore.Hosting;
namespace Umbraco.Cms.Web.Common.Extensions
{
/// <summary>
/// Contains extension methods for the <see cref="IWebHostEnvironment" /> interface.
/// </summary>
public static class WebHostEnvironmentExtensions
{
/// <summary>
/// Maps a virtual path to a physical path to the application's web root
/// </summary>
/// <remarks>
/// Depending on the runtime 'web root', this result can vary. For example in Net Framework the web root and the content root are the same, however
/// in netcore the web root is /wwwroot therefore this will Map to a physical path within wwwroot.
/// </remarks>
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));
}
}
}

View File

@@ -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;
/// <summary>
/// Umbraco specific extensions for the <see cref="IHostBuilder"/> interface.
/// </summary>
public static class HostBuilderExtensions
{
/// <summary>
/// Configures an existing <see cref="IHostBuilder"/> with defaults for an Umbraco application.
/// </summary>
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;
}

View File

@@ -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<IHost> _onBuild;
public UmbracoHostBuilderDecorator(IHostBuilder inner, Action<IHost> onBuild = null)
{
_inner = inner;
_onBuild = onBuild;
}
public IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate) =>
_inner.ConfigureAppConfiguration(configureDelegate);
public IHostBuilder ConfigureContainer<TContainerBuilder>(Action<HostBuilderContext, TContainerBuilder> configureDelegate) =>
_inner.ConfigureContainer(configureDelegate);
public IHostBuilder ConfigureHostConfiguration(Action<IConfigurationBuilder> configureDelegate) =>
_inner.ConfigureHostConfiguration(configureDelegate);
public IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate) =>
_inner.ConfigureServices(configureDelegate);
public IHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory) =>
_inner.UseServiceProviderFactory(factory);
public IHostBuilder UseServiceProviderFactory<TContainerBuilder>(Func<HostBuilderContext, IServiceProviderFactory<TContainerBuilder>> factory) =>
_inner.UseServiceProviderFactory(factory);
public IDictionary<object, object> Properties => _inner.Properties;
public IHost Build()
{
IHost host = _inner.Build();
_onBuild?.Invoke(host);
return host;
}
}

View File

@@ -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()));
}

View File

@@ -0,0 +1,14 @@
using Serilog.Core;
using Serilog.Events;
namespace Umbraco.Cms.Web.Common.Logging.Enrichers;
/// <summary>
/// NoOp but useful for tricks to avoid disposal of the global logger.
/// </summary>
internal class NoopEnricher : ILogEventEnricher
{
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
}
}

View File

@@ -0,0 +1,40 @@
using System;
using Serilog;
using Serilog.Extensions.Hosting;
namespace Umbraco.Cms.Web.Common.Logging;
/// <remarks>
/// HACK:
/// Ensures freeze is only called a single time even when resolving a logger from the snapshot container
/// built for <see cref="ModelsBuilder.RefreshingRazorViewEngine"/>.
/// </remarks>
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<LoggerConfiguration, LoggerConfiguration> 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;
}
}
}

View File

@@ -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<Startup>());
}
}

View File

@@ -23,28 +23,8 @@ namespace Umbraco.Tests.Benchmarks
new NullLogger<TypeFinder>(),
new DefaultUmbracoAssemblyProvider(GetType().Assembly, NullLoggerFactory.Instance));
var cache = new ObjectCacheAppCache();
_typeLoader1 = new TypeLoader(
typeFinder1,
new VaryingRuntimeHash(),
cache,
null,
new NullLogger<TypeLoader>(),
new NoopProfiler());
// populate the cache
cache.Insert(
_typeLoader1.CacheKey,
GetCache,
TimeSpan.FromDays(1));
_typeLoader2 = new TypeLoader(
typeFinder1,
new VaryingRuntimeHash(),
NoAppCache.Instance,
null,
new NullLogger<TypeLoader>(),
new NoopProfiler());
_typeLoader1 = new TypeLoader(typeFinder1, NullLogger<TypeLoader>.Instance);
_typeLoader2 = new TypeLoader(typeFinder1, NullLogger<TypeLoader>.Instance);
}
/// <summary>

View File

@@ -15,7 +15,7 @@ namespace Umbraco.Cms.Tests.Common.Testing
IOptionsMonitor<HostingSettings> hostingSettings,
IOptionsMonitor<WebRoutingSettings> webRoutingSettings,
IWebHostEnvironment webHostEnvironment)
: base(null, hostingSettings, webRoutingSettings, webHostEnvironment)
: base(hostingSettings, webRoutingSettings, webHostEnvironment)
{
}

View File

@@ -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

View File

@@ -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<IConflictingRouteService, TestConflictingRouteService>();
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()

View File

@@ -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, @"<?xml version=""1.0"" encoding=""utf-8""?>
<plugins>
<baseType type=""umbraco.interfaces.ICacheRefresher"">
<add type=""umbraco.macroCacheRefresh, umbraco, Version=6.0.0.0, Culture=neutral, PublicKeyToken=null"" />
</baseType>
</plugins>");
Assert.IsEmpty(_typeLoader.ReadCache()); // uber-legacy cannot be read
File.Delete(filePath);
File.WriteAllText(filePath, @"<?xml version=""1.0"" encoding=""utf-8""?>
<plugins>
<baseType type=""umbraco.interfaces.ICacheRefresher"" resolutionType=""FindAllTypes"">
<add type=""umbraco.macroCacheRefresh, umbraco, Version=6.0.0.0, Culture=neutral, PublicKeyToken=null"" />
</baseType>
</plugins>");
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<IEnumerable<string>> plugins = _typeLoader.TryGetCached(typeof(object), null);
Attempt<IEnumerable<string>> 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<string> 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()