diff --git a/src/Umbraco.Core/Services/IRuntimeState.cs b/src/Umbraco.Core/Services/IRuntimeState.cs index 4b57908ea7..fd817f1986 100644 --- a/src/Umbraco.Core/Services/IRuntimeState.cs +++ b/src/Umbraco.Core/Services/IRuntimeState.cs @@ -50,5 +50,10 @@ namespace Umbraco.Core /// BootFailedException BootFailedException { get; } + /// + /// Determines the runtime level. + /// + void DetermineRuntimeLevel(); + } } diff --git a/src/Umbraco.Core/SimpleMainDom.cs b/src/Umbraco.Core/SimpleMainDom.cs index e6bdda67d7..a2ef0b8d78 100644 --- a/src/Umbraco.Core/SimpleMainDom.cs +++ b/src/Umbraco.Core/SimpleMainDom.cs @@ -8,11 +8,12 @@ namespace Umbraco.Core /// /// Provides a simple implementation of . /// - public class SimpleMainDom : IMainDom + public class SimpleMainDom : IMainDom, IDisposable { private readonly object _locko = new object(); private readonly List> _callbacks = new List>(); private bool _isStopping; + private bool _disposedValue; /// public bool IsMainDom { get; private set; } = true; @@ -59,5 +60,24 @@ namespace Umbraco.Core IsMainDom = false; } } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + Stop(); + } + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } } } diff --git a/src/Umbraco.Examine.Lucene/ExamineLuceneComposer.cs b/src/Umbraco.Examine.Lucene/ExamineLuceneComposer.cs index b7d9cde9d1..b1254a4beb 100644 --- a/src/Umbraco.Examine.Lucene/ExamineLuceneComposer.cs +++ b/src/Umbraco.Examine.Lucene/ExamineLuceneComposer.cs @@ -20,6 +20,7 @@ namespace Umbraco.Examine composition.RegisterUnique(); composition.RegisterUnique(); composition.RegisterUnique(); + composition.RegisterUnique(); } } } diff --git a/src/Umbraco.Examine.Lucene/ILuceneDirectoryFactory.cs b/src/Umbraco.Examine.Lucene/ILuceneDirectoryFactory.cs new file mode 100644 index 0000000000..f4946d491e --- /dev/null +++ b/src/Umbraco.Examine.Lucene/ILuceneDirectoryFactory.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Examine +{ + public interface ILuceneDirectoryFactory + { + Lucene.Net.Store.Directory CreateDirectory(string indexName); + } +} diff --git a/src/Umbraco.Examine.Lucene/LuceneFileSystemDirectoryFactory.cs b/src/Umbraco.Examine.Lucene/LuceneFileSystemDirectoryFactory.cs new file mode 100644 index 0000000000..8f3bf8bf69 --- /dev/null +++ b/src/Umbraco.Examine.Lucene/LuceneFileSystemDirectoryFactory.cs @@ -0,0 +1,69 @@ +using Umbraco.Core.Configuration; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.Hosting; +using Lucene.Net.Store; +using System.IO; +using System; +using Examine.LuceneEngine.Directories; + +namespace Umbraco.Examine +{ + + public class LuceneFileSystemDirectoryFactory : ILuceneDirectoryFactory + { + private readonly ITypeFinder _typeFinder; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IIndexCreatorSettings _settings; + + public LuceneFileSystemDirectoryFactory(ITypeFinder typeFinder, IHostingEnvironment hostingEnvironment, IIndexCreatorSettings settings) + { + _typeFinder = typeFinder; + _hostingEnvironment = hostingEnvironment; + _settings = settings; + } + + public Lucene.Net.Store.Directory CreateDirectory(string indexName) => CreateFileSystemLuceneDirectory(indexName); + + /// + /// Creates a file system based Lucene with the correct locking guidelines for Umbraco + /// + /// + /// The folder name to store the index (single word, not a fully qualified folder) (i.e. Internal) + /// + /// + public virtual Lucene.Net.Store.Directory CreateFileSystemLuceneDirectory(string folderName) + { + + var dirInfo = new DirectoryInfo(Path.Combine(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempData), "ExamineIndexes", folderName)); + if (!dirInfo.Exists) + System.IO.Directory.CreateDirectory(dirInfo.FullName); + + //check if there's a configured directory factory, if so create it and use that to create the lucene dir + var configuredDirectoryFactory = _settings.LuceneDirectoryFactory; + + if (!configuredDirectoryFactory.IsNullOrWhiteSpace()) + { + //this should be a fully qualified type + var factoryType = _typeFinder.GetTypeByName(configuredDirectoryFactory); + if (factoryType == null) throw new NullReferenceException("No directory type found for value: " + configuredDirectoryFactory); + var directoryFactory = (IDirectoryFactory)Activator.CreateInstance(factoryType); + return directoryFactory.CreateDirectory(dirInfo); + } + + //no dir factory, just create a normal fs directory + + var luceneDir = new SimpleFSDirectory(dirInfo); + + //we want to tell examine to use a different fs lock instead of the default NativeFSFileLock which could cause problems if the appdomain + //terminates and in some rare cases would only allow unlocking of the file if IIS is forcefully terminated. Instead we'll rely on the simplefslock + //which simply checks the existence of the lock file + // The full syntax of this is: new NoPrefixSimpleFsLockFactory(dirInfo) + // however, we are setting the DefaultLockFactory in startup so we'll use that instead since it can be managed globally. + luceneDir.SetLockFactory(DirectoryFactory.DefaultLockFactory(dirInfo)); + return luceneDir; + + + } + } +} diff --git a/src/Umbraco.Examine.Lucene/LuceneIndexCreator.cs b/src/Umbraco.Examine.Lucene/LuceneIndexCreator.cs index 9efcdc4891..fe35e9ca0f 100644 --- a/src/Umbraco.Examine.Lucene/LuceneIndexCreator.cs +++ b/src/Umbraco.Examine.Lucene/LuceneIndexCreator.cs @@ -29,47 +29,6 @@ namespace Umbraco.Examine _settings = settings; } - public abstract IEnumerable Create(); - - /// - /// Creates a file system based Lucene with the correct locking guidelines for Umbraco - /// - /// - /// The folder name to store the index (single word, not a fully qualified folder) (i.e. Internal) - /// - /// - public virtual Lucene.Net.Store.Directory CreateFileSystemLuceneDirectory(string folderName) - { - - var dirInfo = new DirectoryInfo(Path.Combine(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempData), "ExamineIndexes", folderName)); - if (!dirInfo.Exists) - System.IO.Directory.CreateDirectory(dirInfo.FullName); - - //check if there's a configured directory factory, if so create it and use that to create the lucene dir - var configuredDirectoryFactory = _settings.LuceneDirectoryFactory; - - if (!configuredDirectoryFactory.IsNullOrWhiteSpace()) - { - //this should be a fully qualified type - var factoryType = _typeFinder.GetTypeByName(configuredDirectoryFactory); - if (factoryType == null) throw new NullReferenceException("No directory type found for value: " + configuredDirectoryFactory); - var directoryFactory = (IDirectoryFactory)Activator.CreateInstance(factoryType); - return directoryFactory.CreateDirectory(dirInfo); - } - - //no dir factory, just create a normal fs directory - - var luceneDir = new SimpleFSDirectory(dirInfo); - - //we want to tell examine to use a different fs lock instead of the default NativeFSFileLock which could cause problems if the appdomain - //terminates and in some rare cases would only allow unlocking of the file if IIS is forcefully terminated. Instead we'll rely on the simplefslock - //which simply checks the existence of the lock file - // The full syntax of this is: new NoPrefixSimpleFsLockFactory(dirInfo) - // however, we are setting the DefaultLockFactory in startup so we'll use that instead since it can be managed globally. - luceneDir.SetLockFactory(DirectoryFactory.DefaultLockFactory(dirInfo)); - return luceneDir; - - - } + public abstract IEnumerable Create(); } } diff --git a/src/Umbraco.Examine.Lucene/LuceneRAMDirectoryFactory.cs b/src/Umbraco.Examine.Lucene/LuceneRAMDirectoryFactory.cs new file mode 100644 index 0000000000..327328390b --- /dev/null +++ b/src/Umbraco.Examine.Lucene/LuceneRAMDirectoryFactory.cs @@ -0,0 +1,24 @@ +using Lucene.Net.Store; +using System; + +namespace Umbraco.Examine +{ + public class LuceneRAMDirectoryFactory : ILuceneDirectoryFactory + { + public LuceneRAMDirectoryFactory() + { + + } + + public Lucene.Net.Store.Directory CreateDirectory(string indexName) => new RandomIdRAMDirectory(); + + private class RandomIdRAMDirectory : RAMDirectory + { + private readonly string _lockId = Guid.NewGuid().ToString(); + public override string GetLockId() + { + return _lockId; + } + } + } +} diff --git a/src/Umbraco.Examine.Lucene/UmbracoIndexesCreator.cs b/src/Umbraco.Examine.Lucene/UmbracoIndexesCreator.cs index 3cf7d6d386..4bb14a3a08 100644 --- a/src/Umbraco.Examine.Lucene/UmbracoIndexesCreator.cs +++ b/src/Umbraco.Examine.Lucene/UmbracoIndexesCreator.cs @@ -12,6 +12,7 @@ using Umbraco.Core.IO; namespace Umbraco.Examine { + /// /// Creates the indexes used by Umbraco /// @@ -28,7 +29,8 @@ namespace Umbraco.Examine IUmbracoIndexConfig umbracoIndexConfig, IHostingEnvironment hostingEnvironment, IRuntimeState runtimeState, - IIndexCreatorSettings settings) : base(typeFinder, hostingEnvironment, settings) + IIndexCreatorSettings settings, + ILuceneDirectoryFactory directoryFactory) : base(typeFinder, hostingEnvironment, settings) { ProfilingLogger = profilingLogger ?? throw new System.ArgumentNullException(nameof(profilingLogger)); LanguageService = languageService ?? throw new System.ArgumentNullException(nameof(languageService)); @@ -37,11 +39,13 @@ namespace Umbraco.Examine UmbracoIndexConfig = umbracoIndexConfig; HostingEnvironment = hostingEnvironment ?? throw new System.ArgumentNullException(nameof(hostingEnvironment)); RuntimeState = runtimeState ?? throw new System.ArgumentNullException(nameof(runtimeState)); + DirectoryFactory = directoryFactory; } protected IProfilingLogger ProfilingLogger { get; } protected IHostingEnvironment HostingEnvironment { get; } protected IRuntimeState RuntimeState { get; } + protected ILuceneDirectoryFactory DirectoryFactory { get; } protected ILocalizationService LanguageService { get; } protected IPublicAccessService PublicAccessService { get; } protected IMemberService MemberService { get; } @@ -65,7 +69,7 @@ namespace Umbraco.Examine { var index = new UmbracoContentIndex( Constants.UmbracoIndexes.InternalIndexName, - CreateFileSystemLuceneDirectory(Constants.UmbracoIndexes.InternalIndexPath), + DirectoryFactory.CreateDirectory(Constants.UmbracoIndexes.InternalIndexPath), new UmbracoFieldDefinitionCollection(), new CultureInvariantWhitespaceAnalyzer(), ProfilingLogger, @@ -81,7 +85,7 @@ namespace Umbraco.Examine { var index = new UmbracoContentIndex( Constants.UmbracoIndexes.ExternalIndexName, - CreateFileSystemLuceneDirectory(Constants.UmbracoIndexes.ExternalIndexPath), + DirectoryFactory.CreateDirectory(Constants.UmbracoIndexes.ExternalIndexPath), new UmbracoFieldDefinitionCollection(), new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30), ProfilingLogger, @@ -97,7 +101,7 @@ namespace Umbraco.Examine var index = new UmbracoMemberIndex( Constants.UmbracoIndexes.MembersIndexName, new UmbracoFieldDefinitionCollection(), - CreateFileSystemLuceneDirectory(Constants.UmbracoIndexes.MembersIndexPath), + DirectoryFactory.CreateDirectory(Constants.UmbracoIndexes.MembersIndexPath), new CultureInvariantWhitespaceAnalyzer(), ProfilingLogger, HostingEnvironment, diff --git a/src/Umbraco.Infrastructure/CompositionExtensions_Essentials.cs b/src/Umbraco.Infrastructure/CompositionExtensions_Essentials.cs index 88ae80bf8b..b2ded07034 100644 --- a/src/Umbraco.Infrastructure/CompositionExtensions_Essentials.cs +++ b/src/Umbraco.Infrastructure/CompositionExtensions_Essentials.cs @@ -31,6 +31,7 @@ namespace Umbraco.Core TypeLoader typeLoader, IRuntimeState state, ITypeFinder typeFinder, + IIOHelper ioHelper, IUmbracoVersion umbracoVersion, IDbProviderFactoryCreator dbProviderFactoryCreator, diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs index 2f9766e5c2..ff86f87569 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs @@ -5,6 +5,7 @@ using System.IO; using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; +using Umbraco.Core.Events; using Umbraco.Core.Exceptions; using Umbraco.Core.Hosting; using Umbraco.Core.IO; @@ -23,7 +24,8 @@ namespace Umbraco.Core.Runtime { private ComponentCollection _components; private IFactory _factory; - private readonly RuntimeState _state; + // runtime state, this instance will get replaced again once the essential services are available to run the check + private RuntimeState _state = RuntimeState.Booting(); private readonly IUmbracoBootPermissionChecker _umbracoBootPermissionChecker; private readonly IRequestCache _requestCache; private readonly IGlobalSettings _globalSettings; @@ -61,14 +63,6 @@ namespace Umbraco.Core.Runtime _globalSettings = Configs.Global(); _connectionStrings = configs.ConnectionStrings(); - - // runtime state - // beware! must use '() => _factory.GetInstance()' and NOT '_factory.GetInstance' - // as the second one captures the current value (null) and therefore fails - _state = new RuntimeState(Configs.Global(), UmbracoVersion) - { - Level = RuntimeLevel.Boot - }; } /// @@ -88,7 +82,7 @@ namespace Umbraco.Core.Runtime /// /// Gets the profiling logger. /// - protected IProfilingLogger ProfilingLogger { get; private set; } + public IProfilingLogger ProfilingLogger { get; private set; } /// /// Gets the @@ -100,8 +94,8 @@ namespace Umbraco.Core.Runtime /// protected IIOHelper IOHelper { get; } protected IHostingEnvironment HostingEnvironment { get; } - protected Configs Configs { get; } - protected IUmbracoVersion UmbracoVersion { get; } + public Configs Configs { get; } + public IUmbracoVersion UmbracoVersion { get; } /// public IRuntimeState State => _state; @@ -164,31 +158,36 @@ namespace Umbraco.Core.Runtime try { - - // run handlers - RuntimeOptions.DoRuntimeBoot(ProfilingLogger); + OnRuntimeBoot(); // application caches - var appCaches = GetAppCaches(); + var appCaches = CreateAppCaches(); // database factory - var databaseFactory = GetDatabaseFactory(); + var databaseFactory = CreateDatabaseFactory(); // type finder/loader var typeLoader = new TypeLoader(TypeFinder, appCaches.RuntimeCache, new DirectoryInfo(HostingEnvironment.LocalTempPath), ProfilingLogger); + // re-create the state object with the essential services + _state = new RuntimeState(Configs.Global(), UmbracoVersion, databaseFactory, Logger); + // create the composition composition = new Composition(register, typeLoader, ProfilingLogger, _state, Configs, IOHelper, appCaches); + composition.RegisterEssentials(Logger, Profiler, ProfilingLogger, MainDom, appCaches, databaseFactory, typeLoader, _state, TypeFinder, IOHelper, UmbracoVersion, DbProviderFactoryCreator, HostingEnvironment, BackOfficeInfo); // register ourselves (TODO: Should we put this in RegisterEssentials?) composition.Register(_ => this, Lifetime.Singleton); + // run handlers + OnRuntimeEssentials(composition, appCaches, typeLoader, databaseFactory); + try { // determine our runtime level - DetermineRuntimeLevel(databaseFactory, ProfilingLogger); + DetermineRuntimeLevel(databaseFactory); } finally { @@ -243,9 +242,6 @@ namespace Umbraco.Core.Runtime // throws if not full-trust _umbracoBootPermissionChecker.ThrowIfNotPermissions(); - // run handlers - RuntimeOptions.DoRuntimeEssentials(_factory); - var hostingEnvironmentLifetime = _factory.TryGetInstance(); if (hostingEnvironmentLifetime == null) throw new InvalidOperationException($"An instance of {typeof(IApplicationShutdownRegistry)} could not be resolved from the container, ensure that one if registered in your runtime before calling {nameof(IRuntime)}.{nameof(Start)}"); @@ -257,7 +253,6 @@ namespace Umbraco.Core.Runtime _components = _factory.GetInstance(); _components.Initialize(); - // now (and only now) is the time to switch over to perWebRequest scopes. // up until that point we may not have a request, and scoped services would // fail to resolve - but we run Initialize within a factory scope - and then, @@ -313,31 +308,29 @@ namespace Umbraco.Core.Runtime } } - // internal for tests - internal void DetermineRuntimeLevel(IUmbracoDatabaseFactory databaseFactory, IProfilingLogger profilingLogger) + private void DetermineRuntimeLevel(IUmbracoDatabaseFactory databaseFactory) { - using (var timer = profilingLogger.DebugDuration("Determining runtime level.", "Determined.")) + using var timer = ProfilingLogger.DebugDuration("Determining runtime level.", "Determined."); + + try { - try - { - _state.DetermineRuntimeLevel(databaseFactory, profilingLogger); + _state.DetermineRuntimeLevel(); - profilingLogger.Debug("Runtime level: {RuntimeLevel} - {RuntimeLevelReason}", _state.Level, _state.Reason); + ProfilingLogger.Debug("Runtime level: {RuntimeLevel} - {RuntimeLevelReason}", _state.Level, _state.Reason); - if (_state.Level == RuntimeLevel.Upgrade) - { - profilingLogger.Debug("Configure database factory for upgrades."); - databaseFactory.ConfigureForUpgrade(); - } - } - catch + if (_state.Level == RuntimeLevel.Upgrade) { - _state.Level = RuntimeLevel.BootFailed; - _state.Reason = RuntimeLevelReason.BootFailedOnException; - timer?.Fail(); - throw; + ProfilingLogger.Debug("Configure database factory for upgrades."); + databaseFactory.ConfigureForUpgrade(); } } + catch + { + _state.Level = RuntimeLevel.BootFailed; + _state.Reason = RuntimeLevelReason.BootFailedOnException; + timer?.Fail(); + throw; + } } private IEnumerable ResolveComposerTypes(TypeLoader typeLoader) @@ -374,9 +367,9 @@ namespace Umbraco.Core.Runtime => typeLoader.GetTypes(); /// - /// Gets the application caches. + /// Creates the application caches. /// - protected virtual AppCaches GetAppCaches() + protected virtual AppCaches CreateAppCaches() { // need the deep clone runtime cache provider to ensure entities are cached properly, ie // are cloned in and cloned out - no request-based cache here since no web-based context, @@ -400,14 +393,33 @@ namespace Umbraco.Core.Runtime => null; /// - /// Gets the database factory. + /// Creates the database factory. /// /// This is strictly internal, for tests only. - protected internal virtual IUmbracoDatabaseFactory GetDatabaseFactory() + protected internal virtual IUmbracoDatabaseFactory CreateDatabaseFactory() => new UmbracoDatabaseFactory(Logger, _globalSettings, _connectionStrings, new Lazy(() => _factory.GetInstance()), DbProviderFactoryCreator); #endregion + #region Events + + protected void OnRuntimeBoot() + { + RuntimeOptions.DoRuntimeBoot(ProfilingLogger); + RuntimeBooting?.Invoke(this, ProfilingLogger); + } + + protected void OnRuntimeEssentials(Composition composition, AppCaches appCaches, TypeLoader typeLoader, IUmbracoDatabaseFactory databaseFactory) + { + RuntimeOptions.DoRuntimeEssentials(composition, appCaches, typeLoader, databaseFactory); + RuntimeEssentials?.Invoke(this, new RuntimeEssentialsEventArgs(composition, appCaches, typeLoader, databaseFactory)); + } + + public event TypedEventHandler RuntimeBooting; + public event TypedEventHandler RuntimeEssentials; + + #endregion + } } diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeEssentialsEventArgs.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeEssentialsEventArgs.cs new file mode 100644 index 0000000000..78d068cb9c --- /dev/null +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeEssentialsEventArgs.cs @@ -0,0 +1,23 @@ +using System; +using Umbraco.Core.Cache; +using Umbraco.Core.Composing; +using Umbraco.Core.Persistence; + +namespace Umbraco.Core.Runtime +{ + public class RuntimeEssentialsEventArgs : EventArgs + { + public RuntimeEssentialsEventArgs(Composition composition, AppCaches appCaches, TypeLoader typeLoader, IUmbracoDatabaseFactory databaseFactory) + { + Composition = composition; + AppCaches = appCaches; + TypeLoader = typeLoader; + DatabaseFactory = databaseFactory; + } + + public Composition Composition { get; } + public AppCaches AppCaches { get; } + public TypeLoader TypeLoader { get; } + public IUmbracoDatabaseFactory DatabaseFactory { get; } + } +} diff --git a/src/Umbraco.Infrastructure/RuntimeOptions.cs b/src/Umbraco.Infrastructure/RuntimeOptions.cs index 562a7e3c5c..23abd474a4 100644 --- a/src/Umbraco.Infrastructure/RuntimeOptions.cs +++ b/src/Umbraco.Infrastructure/RuntimeOptions.cs @@ -16,7 +16,7 @@ namespace Umbraco.Core public static class RuntimeOptions { private static List> _onBoot; - private static List> _onEssentials; + private static List> _onEssentials; /// /// Executes the RuntimeBoot handlers. @@ -33,13 +33,13 @@ namespace Umbraco.Core /// /// Executes the RuntimeEssentials handlers. /// - internal static void DoRuntimeEssentials(IFactory factory) + internal static void DoRuntimeEssentials(Composition composition, AppCaches appCaches, TypeLoader typeLoader, IUmbracoDatabaseFactory databaseFactory) { if (_onEssentials== null) return; foreach (var action in _onEssentials) - action(factory); + action(composition, appCaches, typeLoader, databaseFactory); } /// @@ -64,10 +64,10 @@ namespace Umbraco.Core /// essential things (AppCaches, a TypeLoader, and a database factory) but /// before anything else. /// - public static void OnRuntimeEssentials(Action action) + public static void OnRuntimeEssentials(Action action) { if (_onEssentials == null) - _onEssentials = new List>(); + _onEssentials = new List>(); _onEssentials.Add(action); } } diff --git a/src/Umbraco.Infrastructure/RuntimeState.cs b/src/Umbraco.Infrastructure/RuntimeState.cs index 4a33291314..9b88430080 100644 --- a/src/Umbraco.Infrastructure/RuntimeState.cs +++ b/src/Umbraco.Infrastructure/RuntimeState.cs @@ -1,10 +1,8 @@ using System; using System.Threading; using Semver; -using Umbraco.Core.Collections; using Umbraco.Core.Configuration; using Umbraco.Core.Exceptions; -using Umbraco.Core.Hosting; using Umbraco.Core.Logging; using Umbraco.Core.Migrations.Upgrade; using Umbraco.Core.Persistence; @@ -18,14 +16,27 @@ namespace Umbraco.Core { private readonly IGlobalSettings _globalSettings; private readonly IUmbracoVersion _umbracoVersion; + private readonly IUmbracoDatabaseFactory _databaseFactory; + private readonly ILogger _logger; + + /// + /// The initial + /// + public static RuntimeState Booting() => new RuntimeState() { Level = RuntimeLevel.Boot }; + + private RuntimeState() + { + } /// /// Initializes a new instance of the class. /// - public RuntimeState(IGlobalSettings globalSettings, IUmbracoVersion umbracoVersion) + public RuntimeState(IGlobalSettings globalSettings, IUmbracoVersion umbracoVersion, IUmbracoDatabaseFactory databaseFactory, ILogger logger) { _globalSettings = globalSettings; _umbracoVersion = umbracoVersion; + _databaseFactory = databaseFactory; + _logger = logger; } @@ -53,16 +64,14 @@ namespace Umbraco.Core /// public BootFailedException BootFailedException { get; internal set; } - /// - /// Determines the runtime level. - /// - public void DetermineRuntimeLevel(IUmbracoDatabaseFactory databaseFactory, ILogger logger) + /// + public void DetermineRuntimeLevel() { - if (databaseFactory.Configured == false) + if (_databaseFactory.Configured == false) { // local version *does* match code version, but the database is not configured // install - may happen with Deploy/Cloud/etc - logger.Debug("Database is not configured, need to install Umbraco."); + _logger.Debug("Database is not configured, need to install Umbraco."); Level = RuntimeLevel.Install; Reason = RuntimeLevelReason.InstallNoDatabase; return; @@ -75,16 +84,16 @@ namespace Umbraco.Core var tries = _globalSettings.InstallMissingDatabase ? 2 : 5; for (var i = 0;;) { - connect = databaseFactory.CanConnect; + connect = _databaseFactory.CanConnect; if (connect || ++i == tries) break; - logger.Debug("Could not immediately connect to database, trying again."); + _logger.Debug("Could not immediately connect to database, trying again."); Thread.Sleep(1000); } if (connect == false) { // cannot connect to configured database, this is bad, fail - logger.Debug("Could not connect to database."); + _logger.Debug("Could not connect to database."); if (_globalSettings.InstallMissingDatabase) { @@ -108,12 +117,12 @@ namespace Umbraco.Core bool noUpgrade; try { - noUpgrade = EnsureUmbracoUpgradeState(databaseFactory, logger); + noUpgrade = EnsureUmbracoUpgradeState(_databaseFactory, _logger); } catch (Exception e) { // can connect to the database but cannot check the upgrade state... oops - logger.Warn(e, "Could not check the upgrade state."); + _logger.Warn(e, "Could not check the upgrade state."); if (_globalSettings.InstallEmptyDatabase) { @@ -145,7 +154,7 @@ namespace Umbraco.Core // although the files version matches the code version, the database version does not // which means the local files have been upgraded but not the database - need to upgrade - logger.Debug("Has not reached the final upgrade step, need to upgrade Umbraco."); + _logger.Debug("Has not reached the final upgrade step, need to upgrade Umbraco."); Level = RuntimeLevel.Upgrade; Reason = RuntimeLevelReason.UpgradeMigrations; } diff --git a/src/Umbraco.Infrastructure/Scheduling/SchedulerComponent.cs b/src/Umbraco.Infrastructure/Scheduling/SchedulerComponent.cs index a5850719ab..3539993358 100644 --- a/src/Umbraco.Infrastructure/Scheduling/SchedulerComponent.cs +++ b/src/Umbraco.Infrastructure/Scheduling/SchedulerComponent.cs @@ -18,7 +18,7 @@ using Umbraco.Web.Routing; namespace Umbraco.Web.Scheduling { - internal sealed class SchedulerComponent : IComponent + public sealed class SchedulerComponent : IComponent { private const int DefaultDelayMilliseconds = 180000; // 3 mins private const int OneMinuteMilliseconds = 60000; diff --git a/src/Umbraco.Infrastructure/Search/BackgroundIndexRebuilder.cs b/src/Umbraco.Infrastructure/Search/BackgroundIndexRebuilder.cs index 1946e2041b..d18480eb82 100644 --- a/src/Umbraco.Infrastructure/Search/BackgroundIndexRebuilder.cs +++ b/src/Umbraco.Infrastructure/Search/BackgroundIndexRebuilder.cs @@ -12,7 +12,7 @@ namespace Umbraco.Web.Search /// /// Utility to rebuild all indexes on a background thread /// - public sealed class BackgroundIndexRebuilder + public class BackgroundIndexRebuilder { private static readonly object RebuildLocker = new object(); private readonly IndexRebuilder _indexRebuilder; @@ -34,7 +34,7 @@ namespace Umbraco.Web.Search /// /// /// - public void RebuildIndexes(bool onlyEmptyIndexes, int waitMilliseconds = 0) + public virtual void RebuildIndexes(bool onlyEmptyIndexes, int waitMilliseconds = 0) { // TODO: need a way to disable rebuilding on startup diff --git a/src/Umbraco.Tests.Common/TestHelperBase.cs b/src/Umbraco.Tests.Common/TestHelperBase.cs index 85a463ddfa..cb8b6618ee 100644 --- a/src/Umbraco.Tests.Common/TestHelperBase.cs +++ b/src/Umbraco.Tests.Common/TestHelperBase.cs @@ -47,13 +47,6 @@ namespace Umbraco.Tests.Common public Configs GetConfigs() => GetConfigsFactory().Create(); - public IRuntimeState GetRuntimeState() - { - return new RuntimeState( - Mock.Of(), - GetUmbracoVersion()); - } - public abstract IBackOfficeInfo GetBackOfficeInfo(); public IConfigsFactory GetConfigsFactory() => new ConfigsFactory(); diff --git a/src/Umbraco.Tests.Integration/ContainerTests.cs b/src/Umbraco.Tests.Integration/ContainerTests.cs index 2098b7241e..3ba3c31d03 100644 --- a/src/Umbraco.Tests.Integration/ContainerTests.cs +++ b/src/Umbraco.Tests.Integration/ContainerTests.cs @@ -83,7 +83,7 @@ namespace Umbraco.Tests.Integration // it means the container won't be disposed, and maybe other services? not sure. // In cases where we use it can we use IConfigureOptions? https://andrewlock.net/access-services-inside-options-and-startup-using-configureoptions/ - var umbracoContainer = UmbracoIntegrationTest.GetUmbracoContainer(out var serviceProviderFactory); + var umbracoContainer = UmbracoIntegrationTest.CreateUmbracoContainer(out var serviceProviderFactory); IHostApplicationLifetime lifetime1 = null; diff --git a/src/Umbraco.Tests.Integration/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Tests.Integration/Extensions/ApplicationBuilderExtensions.cs deleted file mode 100644 index edbdcd0722..0000000000 --- a/src/Umbraco.Tests.Integration/Extensions/ApplicationBuilderExtensions.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System; -using System.Data.Common; -using System.Data.SqlClient; -using System.IO; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; -using Umbraco.Core.Persistence; -using Umbraco.Tests.Integration.Testing; -using Umbraco.Tests.Testing; - -namespace Umbraco.Tests.Integration.Extensions -{ - public static class ApplicationBuilderExtensions - { - /// - /// Creates a LocalDb instance to use for the test - /// - /// - /// - /// - /// - /// - public static IApplicationBuilder UseTestLocalDb(this IApplicationBuilder app, - string workingDirectory, - UmbracoIntegrationTest integrationTest, out string connectionString) - { - connectionString = null; - var dbFilePath = Path.Combine(workingDirectory, "LocalDb"); - - // get the currently set db options - var testOptions = TestOptionAttributeBase.GetTestOptions(); - - if (testOptions.Database == UmbracoTestOptions.Database.None) - return app; - - // need to manually register this factory - DbProviderFactories.RegisterFactory(Constants.DbProviderNames.SqlServer, SqlClientFactory.Instance); - - if (!Directory.Exists(dbFilePath)) - Directory.CreateDirectory(dbFilePath); - - var db = UmbracoIntegrationTest.GetOrCreate(dbFilePath, - app.ApplicationServices.GetRequiredService(), - app.ApplicationServices.GetRequiredService(), - app.ApplicationServices.GetRequiredService()); - - - switch (testOptions.Database) - { - case UmbracoTestOptions.Database.NewSchemaPerTest: - - // New DB + Schema - var newSchemaDbId = db.AttachSchema(); - - // Add teardown callback - integrationTest.OnTestTearDown(() => db.Detach(newSchemaDbId)); - - // We must re-configure our current factory since attaching a new LocalDb from the pool changes connection strings - var dbFactory = app.ApplicationServices.GetRequiredService(); - if (!dbFactory.Configured) - { - dbFactory.Configure(db.ConnectionString, Constants.DatabaseProviders.SqlServer); - } - - // In the case that we've initialized the schema, it means that we are installed so we'll want to ensure that - // the runtime state is configured correctly so we'll force update the configuration flag and re-run the - // runtime state checker. - // TODO: This wouldn't be required if we don't store the Umbraco version in config - - // right now we are an an 'Install' state - var runtimeState = (RuntimeState)app.ApplicationServices.GetRequiredService(); - Assert.AreEqual(RuntimeLevel.Install, runtimeState.Level); - - // dynamically change the config status - var umbVersion = app.ApplicationServices.GetRequiredService(); - var config = app.ApplicationServices.GetRequiredService(); - config[Constants.Configuration.ConfigGlobalPrefix + "ConfigurationStatus"] = umbVersion.SemanticVersion.ToString(); - - // re-run the runtime level check - var profilingLogger = app.ApplicationServices.GetRequiredService(); - runtimeState.DetermineRuntimeLevel(dbFactory, profilingLogger); - - Assert.AreEqual(RuntimeLevel.Run, runtimeState.Level); - - break; - case UmbracoTestOptions.Database.NewEmptyPerTest: - - var newEmptyDbId = db.AttachEmpty(); - - // Add teardown callback - integrationTest.OnTestTearDown(() => db.Detach(newEmptyDbId)); - - - break; - case UmbracoTestOptions.Database.NewSchemaPerFixture: - - // New DB + Schema - var newSchemaFixtureDbId = db.AttachSchema(); - - // Add teardown callback - integrationTest.OnFixtureTearDown(() => db.Detach(newSchemaFixtureDbId)); - - break; - case UmbracoTestOptions.Database.NewEmptyPerFixture: - - throw new NotImplementedException(); - - //// Add teardown callback - //integrationTest.OnFixtureTearDown(() => db.Detach()); - - break; - default: - throw new ArgumentOutOfRangeException(nameof(testOptions), testOptions, null); - } - connectionString = db.ConnectionString; - return app; - } - } - - -} diff --git a/src/Umbraco.Tests.Integration/Implementations/TestHelper.cs b/src/Umbraco.Tests.Integration/Implementations/TestHelper.cs index a6cd372411..7faa4dac34 100644 --- a/src/Umbraco.Tests.Integration/Implementations/TestHelper.cs +++ b/src/Umbraco.Tests.Integration/Implementations/TestHelper.cs @@ -19,6 +19,7 @@ using Umbraco.Core.Runtime; using Umbraco.Tests.Common; using Umbraco.Web.Common.AspNetCore; using IHostingEnvironment = Umbraco.Core.Hosting.IHostingEnvironment; +using Microsoft.Extensions.FileProviders; namespace Umbraco.Tests.Integration.Implementations { @@ -39,11 +40,17 @@ namespace Umbraco.Tests.Integration.Implementations _httpContextAccessor = Mock.Of(x => x.HttpContext == httpContext); _ipResolver = new AspNetCoreIpResolver(_httpContextAccessor); + var contentRoot = Assembly.GetExecutingAssembly().GetRootDirectorySafe(); var hostEnvironment = new Mock(); - hostEnvironment.Setup(x => x.ApplicationName).Returns("UmbracoIntegrationTests"); - hostEnvironment.Setup(x => x.ContentRootPath) - .Returns(() => Assembly.GetExecutingAssembly().GetRootDirectorySafe()); + // this must be the assembly name for the WebApplicationFactory to work + hostEnvironment.Setup(x => x.ApplicationName).Returns(GetType().Assembly.GetName().Name); + hostEnvironment.Setup(x => x.ContentRootPath).Returns(() => contentRoot); + hostEnvironment.Setup(x => x.ContentRootFileProvider).Returns(() => new PhysicalFileProvider(contentRoot)); hostEnvironment.Setup(x => x.WebRootPath).Returns(() => WorkingDirectory); + hostEnvironment.Setup(x => x.WebRootFileProvider).Returns(() => new PhysicalFileProvider(WorkingDirectory)); + // we also need to expose it as the obsolete interface since netcore's WebApplicationFactory casts it + hostEnvironment.As(); + _hostEnvironment = hostEnvironment.Object; _hostingLifetime = new AspNetCoreApplicationShutdownRegistry(Mock.Of()); diff --git a/src/Umbraco.Tests.Integration/RuntimeTests.cs b/src/Umbraco.Tests.Integration/RuntimeTests.cs index bd7684fcae..b41ef4ac02 100644 --- a/src/Umbraco.Tests.Integration/RuntimeTests.cs +++ b/src/Umbraco.Tests.Integration/RuntimeTests.cs @@ -88,7 +88,7 @@ namespace Umbraco.Tests.Integration [Test] public async Task AddUmbracoCore() { - var umbracoContainer = UmbracoIntegrationTest.GetUmbracoContainer(out var serviceProviderFactory); + var umbracoContainer = UmbracoIntegrationTest.CreateUmbracoContainer(out var serviceProviderFactory); var testHelper = new TestHelper(); var hostBuilder = new HostBuilder() @@ -128,7 +128,7 @@ namespace Umbraco.Tests.Integration [Test] public async Task UseUmbracoCore() { - var umbracoContainer = UmbracoIntegrationTest.GetUmbracoContainer(out var serviceProviderFactory); + var umbracoContainer = UmbracoIntegrationTest.CreateUmbracoContainer(out var serviceProviderFactory); var testHelper = new TestHelper(); var hostBuilder = new HostBuilder() diff --git a/src/Umbraco.Tests.Integration/TestServerTest/TestAuthHandler.cs b/src/Umbraco.Tests.Integration/TestServerTest/TestAuthHandler.cs new file mode 100644 index 0000000000..08cd49bd81 --- /dev/null +++ b/src/Umbraco.Tests.Integration/TestServerTest/TestAuthHandler.cs @@ -0,0 +1,40 @@ +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Core; +using Umbraco.Core.BackOffice; +using Umbraco.Core.Mapping; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Services; +using Umbraco.Web.Common.Security; + +namespace Umbraco.Tests.Integration.TestServerTest +{ + public class TestAuthHandler : AuthenticationHandler + { + private readonly BackOfficeSignInManager _backOfficeSignInManager; + + private readonly BackOfficeIdentityUser _fakeUser; + public TestAuthHandler(IOptionsMonitor options, + ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, BackOfficeSignInManager backOfficeSignInManager, IUserService userService, UmbracoMapper umbracoMapper) + : base(options, logger, encoder, clock) + { + _backOfficeSignInManager = backOfficeSignInManager; + + var user = userService.GetUserById(Constants.Security.SuperUserId); + _fakeUser = umbracoMapper.Map(user); + _fakeUser.SecurityStamp = "Needed"; + } + + protected override async Task HandleAuthenticateAsync() + { + + var principal = await _backOfficeSignInManager.CreateUserPrincipalAsync(_fakeUser); + var ticket = new AuthenticationTicket(principal, "Test"); + + return AuthenticateResult.Success(ticket); + } + } +} diff --git a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoBuilderExtensions.cs b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoBuilderExtensions.cs new file mode 100644 index 0000000000..7c919e3894 --- /dev/null +++ b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoBuilderExtensions.cs @@ -0,0 +1,40 @@ +using System; +using Umbraco.Core.Cache; +using Umbraco.Core.Composing.LightInject; +using Umbraco.Core.Runtime; +using Umbraco.Extensions; +using Umbraco.Tests.Integration.Implementations; +using Umbraco.Tests.Integration.Testing; +using Umbraco.Web.Common.Builder; + +namespace Umbraco.Tests.Integration.TestServerTest +{ + public static class UmbracoBuilderExtensions + { + /// + /// Uses a test version of Umbraco Core with a test IRuntime + /// + /// + /// + public static IUmbracoBuilder WithTestCore(this IUmbracoBuilder builder, TestHelper testHelper, LightInjectContainer container, + Action dbInstallEventHandler) + { + return builder.AddWith(nameof(global::Umbraco.Web.Common.Builder.UmbracoBuilderExtensions.WithCore), + () => + { + builder.Services.AddUmbracoCore( + builder.WebHostEnvironment, + container, + typeof(UmbracoBuilderExtensions).Assembly, + NoAppCache.Instance, + testHelper.GetLoggingConfiguration(), + // TODO: Yep that's extremely ugly + (a, b, c, d, e, f, g, h, i, j) => + UmbracoIntegrationTest.CreateTestRuntime(a, b, c, d, e, f, g, h, i, j, + testHelper.MainDom, // SimpleMainDom + dbInstallEventHandler), // DB Installation event handler + out _); + }); + } + } +} diff --git a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index c5f413e24f..18a5836223 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -2,22 +2,26 @@ using System; using System.Linq.Expressions; using System.Net.Http; -using System.Reflection; -using Examine; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using Umbraco.Composing; -using Umbraco.Core.Scoping; -using Umbraco.Examine; +using Umbraco.Core; using Umbraco.Extensions; using Umbraco.Tests.Integration.Testing; using Umbraco.Tests.Testing; using Umbraco.Web; +using Umbraco.Web.Common.Builder; using Umbraco.Web.Common.Controllers; - +using Umbraco.Web.Editors; +using Microsoft.Extensions.Hosting; namespace Umbraco.Tests.Integration.TestServerTest { @@ -25,37 +29,56 @@ namespace Umbraco.Tests.Integration.TestServerTest [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Console, Boot = false)] public abstract class UmbracoTestServerTestBase : UmbracoIntegrationTest { - [SetUp] - public void SetUp() + public override Task Setup() { - Factory = new UmbracoWebApplicationFactory(TestDBConnectionString); - Client = Factory.CreateClient(new WebApplicationFactoryClientOptions(){ - AllowAutoRedirect = false + InMemoryConfiguration["ConnectionStrings:" + Constants.System.UmbracoConnectionName] = null; + InMemoryConfiguration["Umbraco:CMS:Hosting:Debug"] = "true"; + + // create new WebApplicationFactory specifying 'this' as the IStartup instance + var factory = new UmbracoWebApplicationFactory(CreateHostBuilder); + + // additional host configuration for web server integration tests + Factory = factory.WithWebHostBuilder(builder => + { + // Executes after the standard ConfigureServices method + builder.ConfigureTestServices(services => + { + services.AddAuthentication("Test").AddScheme("Test", options => { }); + }); }); + + Client = Factory.CreateClient(new WebApplicationFactoryClientOptions + { + AllowAutoRedirect = false + }); + LinkGenerator = Factory.Services.GetRequiredService(); - ExecuteExamineIndexOperationsInSync(); + return Task.CompletedTask; } - private void ExecuteExamineIndexOperationsInSync() + public override IHostBuilder CreateHostBuilder() { - var examineManager = Factory.Services.GetRequiredService(); + var builder = base.CreateHostBuilder(); + builder.ConfigureWebHost(builder => + { + // need to configure the IWebHostEnvironment too + builder.ConfigureServices((c, s) => { + c.HostingEnvironment = TestHelper.GetWebHostEnvironment(); + }); - foreach (var index in examineManager.Indexes) - { - if (index is UmbracoExamineIndex umbracoExamineIndex) - { - umbracoExamineIndex.ProcessNonAsync(); - } - } + // call startup + builder.Configure(app => + { + Services = app.ApplicationServices; + Configure(app); + }); + }) + .UseEnvironment(Environments.Development); + return builder; } - /// - /// Get the service from the underlying container that is also used by the . - /// - protected T GetRequiredService() => Factory.Services.GetRequiredService(); - /// /// Prepare a url before using . /// This returns the url but also sets the HttpContext.request into to use this url. @@ -85,13 +108,14 @@ namespace Umbraco.Tests.Integration.TestServerTest return url; } - protected HttpClient Client { get; set; } - protected LinkGenerator LinkGenerator { get; set; } - protected UmbracoWebApplicationFactory Factory { get; set; } + protected HttpClient Client { get; private set; } + protected LinkGenerator LinkGenerator { get; private set; } + protected WebApplicationFactory Factory { get; private set; } [TearDown] - public void TearDown() + public override void TearDown() { + base.TearDown(); Factory.Dispose(); @@ -99,7 +123,40 @@ namespace Umbraco.Tests.Integration.TestServerTest { Current.IsInitialized = false; } - } + + #region IStartup + + public override void ConfigureServices(IServiceCollection services) + { + //services.AddSingleton(TestHelper.DbProviderFactoryCreator); + //var webHostEnvironment = TestHelper.GetWebHostEnvironment(); + //services.AddRequiredNetCoreServices(TestHelper, webHostEnvironment); + + var umbracoBuilder = services.AddUmbraco(TestHelper.GetWebHostEnvironment(), Configuration); + umbracoBuilder + .WithConfiguration() + .WithTestCore(TestHelper, UmbracoContainer, UseTestLocalDb) // This is the important one! + .WithWebComponents() + .WithRuntimeMinifier() + .WithBackOffice() + .WithBackOfficeIdentity() + //.WithMiniProfiler() + .WithMvcAndRazor(mvcBuilding: mvcBuilder => + { + mvcBuilder.AddApplicationPart(typeof(ContentController).Assembly); + }) + .WithWebServer() + .Build(); + } + + public override void Configure(IApplicationBuilder app) + { + app.UseUmbraco(); + } + + #endregion + + } } diff --git a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoWebApplicationFactory.cs b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoWebApplicationFactory.cs index ea2b64c790..251f155d2c 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoWebApplicationFactory.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoWebApplicationFactory.cs @@ -1,97 +1,23 @@ -using System.Collections.Generic; -using System.Net.Http; -using System.Security.Claims; -using System.Text.Encodings.Web; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Hosting; +using System; using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Umbraco.Core; -using Umbraco.Core.BackOffice; -using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; -using Umbraco.Core.Mapping; -using Umbraco.Core.Models.Membership; -using Umbraco.Core.Services; -using Umbraco.Web.Common.Security; -using Umbraco.Web.UI.NetCore; namespace Umbraco.Tests.Integration.TestServerTest { - public class UmbracoWebApplicationFactory : CustomWebApplicationFactory + + public class UmbracoWebApplicationFactory : WebApplicationFactory where TStartup : class { - public UmbracoWebApplicationFactory(string testDbConnectionString) :base(testDbConnectionString) - { - } - } + private readonly Func _createHostBuilder; - public abstract class CustomWebApplicationFactory : WebApplicationFactory where TStartup : class - { - private readonly string _testDbConnectionString; - - protected CustomWebApplicationFactory(string testDbConnectionString) + /// + /// Constructor to create a new WebApplicationFactory + /// + /// Method to create the IHostBuilder + public UmbracoWebApplicationFactory(Func createHostBuilder) { - _testDbConnectionString = testDbConnectionString; + _createHostBuilder = createHostBuilder; } - protected override void ConfigureWebHost(IWebHostBuilder builder) - { - base.ConfigureWebHost(builder); - - builder.ConfigureTestServices(services => - { - services.AddAuthentication("Test").AddScheme("Test", options => {}); - }); - - builder.ConfigureAppConfiguration(x => - { - x.AddInMemoryCollection(new Dictionary() - { - ["ConnectionStrings:"+ Constants.System.UmbracoConnectionName] = _testDbConnectionString, - ["Umbraco:CMS:Hosting:Debug"] = "true", - }); - }); - - } - - protected override IHostBuilder CreateHostBuilder() - { - - var builder = base.CreateHostBuilder(); - builder.UseUmbraco(); - return builder; - } - } - - public class TestAuthHandler : AuthenticationHandler - { - private readonly BackOfficeSignInManager _backOfficeSignInManager; - - private readonly BackOfficeIdentityUser _fakeUser; - public TestAuthHandler(IOptionsMonitor options, - ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, BackOfficeSignInManager backOfficeSignInManager, IUserService userService, UmbracoMapper umbracoMapper) - : base(options, logger, encoder, clock) - { - _backOfficeSignInManager = backOfficeSignInManager; - - var user = userService.GetUserById(Constants.Security.SuperUserId); - _fakeUser = umbracoMapper.Map(user); - _fakeUser.SecurityStamp = "Needed"; - } - - protected override async Task HandleAuthenticateAsync() - { - - var principal = await _backOfficeSignInManager.CreateUserPrincipalAsync(_fakeUser); - var ticket = new AuthenticationTicket(principal, "Test"); - - return AuthenticateResult.Success(ticket); - } + protected override IHostBuilder CreateHostBuilder() => _createHostBuilder(); } } diff --git a/src/Umbraco.Tests.Integration/Testing/IntegrationTestComponent.cs b/src/Umbraco.Tests.Integration/Testing/IntegrationTestComponent.cs new file mode 100644 index 0000000000..69819c9bef --- /dev/null +++ b/src/Umbraco.Tests.Integration/Testing/IntegrationTestComponent.cs @@ -0,0 +1,43 @@ +using Examine; +using Examine.LuceneEngine.Providers; +using Umbraco.Core.Composing; +using Umbraco.Examine; + +namespace Umbraco.Tests.Integration.Testing +{ + /// + /// A component to customize some services to work nicely with integration tests + /// + public class IntegrationTestComponent : IComponent + { + private readonly IExamineManager _examineManager; + + public IntegrationTestComponent(IExamineManager examineManager) + { + _examineManager = examineManager; + } + + public void Initialize() + { + ConfigureExamineIndexes(); + } + + public void Terminate() + { + } + + /// + /// Configure all indexes to run sync (non-backbround threads) and to use RAMDirectory + /// + private void ConfigureExamineIndexes() + { + foreach (var index in _examineManager.Indexes) + { + if (index is LuceneIndex luceneIndex) + { + luceneIndex.ProcessNonAsync(); + } + } + } + } +} diff --git a/src/Umbraco.Tests.Integration/Testing/IntegrationTestComposer.cs b/src/Umbraco.Tests.Integration/Testing/IntegrationTestComposer.cs index 844877b1fd..97482f9482 100644 --- a/src/Umbraco.Tests.Integration/Testing/IntegrationTestComposer.cs +++ b/src/Umbraco.Tests.Integration/Testing/IntegrationTestComposer.cs @@ -1,7 +1,15 @@ using Moq; using Umbraco.Core; using Umbraco.Core.Composing; +using Umbraco.Core.Configuration; +using Umbraco.Core.Hosting; +using Umbraco.Core.Logging; +using Umbraco.Core.Services; using Umbraco.Core.WebAssets; +using Umbraco.Examine; +using Umbraco.Web.PublishedCache.NuCache; +using Umbraco.Web.Scheduling; +using Umbraco.Web.Search; namespace Umbraco.Tests.Integration.Testing { @@ -13,11 +21,36 @@ namespace Umbraco.Tests.Integration.Testing /// This is a IUserComposer so that it runs after all core composers /// [RuntimeLevel(MinLevel = RuntimeLevel.Boot)] - public class IntegrationTestComposer : IUserComposer + public class IntegrationTestComposer : ComponentComposer { - public void Compose(Composition composition) + public override void Compose(Composition composition) { + base.Compose(composition); + + composition.Components().Remove(); + composition.RegisterUnique(); composition.RegisterUnique(factory => Mock.Of()); + + // we don't want persisted nucache files in tests + composition.Register(factory => new PublishedSnapshotServiceOptions { IgnoreLocalDb = true }); + + // ensure all lucene indexes are using RAM directory (no file system) + composition.RegisterUnique(); } + + // replace the default so there is no background index rebuilder + private class TestBackgroundIndexRebuilder : BackgroundIndexRebuilder + { + public TestBackgroundIndexRebuilder(IMainDom mainDom, IProfilingLogger logger, IApplicationShutdownRegistry hostingEnvironment, IndexRebuilder indexRebuilder) + : base(mainDom, logger, hostingEnvironment, indexRebuilder) + { + } + + public override void RebuildIndexes(bool onlyEmptyIndexes, int waitMilliseconds = 0) + { + // noop + } + } + } } diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index 89cbfa992d..ee03e2a146 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; using NUnit.Framework; using Umbraco.Core.Cache; using Umbraco.Core.Composing; @@ -22,9 +21,19 @@ using Umbraco.Extensions; using Umbraco.Tests.Testing; using Umbraco.Web; using ILogger = Umbraco.Core.Logging.ILogger; +using Umbraco.Core.Runtime; +using Umbraco.Core; +using Moq; +using System.Collections.Generic; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using System.Data.SqlClient; +using System.Data.Common; +using System.IO; namespace Umbraco.Tests.Integration.Testing { + /// /// Abstract class for integration tests /// @@ -35,7 +44,7 @@ namespace Umbraco.Tests.Integration.Testing [NonParallelizable] public abstract class UmbracoIntegrationTest { - public static LightInjectContainer GetUmbracoContainer(out UmbracoServiceProviderFactory serviceProviderFactory) + public static LightInjectContainer CreateUmbracoContainer(out UmbracoServiceProviderFactory serviceProviderFactory) { var container = UmbracoServiceProviderFactory.CreateServiceContainer(); serviceProviderFactory = new UmbracoServiceProviderFactory(container, false); @@ -43,6 +52,191 @@ namespace Umbraco.Tests.Integration.Testing return umbracoContainer; } + private Action _testTeardown = null; + private Action _fixtureTeardown = null; + + public void OnTestTearDown(Action tearDown) => _testTeardown = tearDown; + + public void OnFixtureTearDown(Action tearDown) => _fixtureTeardown = tearDown; + + [OneTimeTearDown] + public void FixtureTearDown() => _fixtureTeardown?.Invoke(); + + [TearDown] + public virtual void TearDown() => _testTeardown?.Invoke(); + + [SetUp] + public virtual async Task Setup() + { + var hostBuilder = CreateHostBuilder(); + var host = await hostBuilder.StartAsync(); + Services = host.Services; + var app = new ApplicationBuilder(host.Services); + Configure(app); + } + + #region Generic Host Builder and Runtime + + /// + /// Create the Generic Host and execute startup ConfigureServices/Configure calls + /// + /// + public virtual IHostBuilder CreateHostBuilder() + { + UmbracoContainer = CreateUmbracoContainer(out var serviceProviderFactory); + _serviceProviderFactory = serviceProviderFactory; + + var hostBuilder = Host.CreateDefaultBuilder() + // 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 + // create separate Host instances. So instead of UseStartup, we just call ConfigureServices/Configure ourselves, + // and in the case of the UmbracoTestServerTestBase it will use the ConfigureWebHost to Configure the IApplicationBuilder directly. + //.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(GetType()); }) + .UseUmbraco(_serviceProviderFactory) + .ConfigureAppConfiguration((context, configBuilder) => + { + context.HostingEnvironment = TestHelper.GetWebHostEnvironment(); + Configuration = context.Configuration; + configBuilder.AddInMemoryCollection(InMemoryConfiguration); + }) + .ConfigureServices((hostContext, services) => + { + ConfigureServices(services); + }); + return hostBuilder; + } + + /// + /// Creates a instance for testing and registers an event handler for database install + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public CoreRuntime CreateTestRuntime(Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, + ILogger logger, IProfiler profiler, Core.Hosting.IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo, + ITypeFinder typeFinder, IRequestCache requestCache, IDbProviderFactoryCreator dbProviderFactoryCreator) + { + return CreateTestRuntime(configs, + umbracoVersion, + ioHelper, + logger, + profiler, + hostingEnvironment, + backOfficeInfo, + typeFinder, + requestCache, + dbProviderFactoryCreator, + TestHelper.MainDom, // SimpleMainDom + UseTestLocalDb // DB Installation event handler + ); + } + + /// + /// Creates a instance for testing and registers an event handler for database install + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// The event handler used for DB installation + /// + public static CoreRuntime CreateTestRuntime(Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, + ILogger logger, IProfiler profiler, Core.Hosting.IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo, + ITypeFinder typeFinder, IRequestCache requestCache, IDbProviderFactoryCreator dbProviderFactoryCreator, + IMainDom mainDom, Action eventHandler) + { + var runtime = new CoreRuntime( + configs, + umbracoVersion, + ioHelper, + logger, + profiler, + Mock.Of(), + hostingEnvironment, + backOfficeInfo, + dbProviderFactoryCreator, + mainDom, + typeFinder, + requestCache); + + runtime.RuntimeEssentials += (sender, args) => eventHandler(sender, args); + + return runtime; + } + + #endregion + + #region IStartup + + public virtual void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(TestHelper.DbProviderFactoryCreator); + var webHostEnvironment = TestHelper.GetWebHostEnvironment(); + services.AddRequiredNetCoreServices(TestHelper, webHostEnvironment); + + // Add it! + services.AddUmbracoConfiguration(Configuration); + services.AddUmbracoCore(webHostEnvironment, UmbracoContainer, GetType().Assembly, NoAppCache.Instance, TestHelper.GetLoggingConfiguration(), CreateTestRuntime, out _); + services.AddUmbracoWebComponents(); + services.AddUmbracoRuntimeMinifier(Configuration); + services.AddUmbracoBackOffice(); + services.AddUmbracoBackOfficeIdentity(); + + services.AddMvc(); + + services.AddSingleton(new ConsoleLogger(new MessageTemplates())); + + CustomTestSetup(services); + } + + public virtual void Configure(IApplicationBuilder app) + { + Services.GetRequiredService().EnsureUmbracoContext(); + + // get the currently set ptions + var testOptions = TestOptionAttributeBase.GetTestOptions(); + if (testOptions.Boot) + { + app.UseUmbracoCore(); + } + } + + #endregion + + #region LocalDb + + + private static readonly object _dbLocker = new object(); + private static LocalDbTestDatabase _dbInstance; + + /// + /// Event handler for the to install the database + /// + /// + /// + protected void UseTestLocalDb(CoreRuntime sender, RuntimeEssentialsEventArgs args) + { + // This will create a db, install the schema and ensure the app is configured to run + InstallTestLocalDb(args.DatabaseFactory, sender.ProfilingLogger, sender.Configs.Global(), sender.State, TestHelper.WorkingDirectory, out var connectionString); + TestDBConnectionString = connectionString; + InMemoryConfiguration["ConnectionStrings:" + Constants.System.UmbracoConnectionName] = TestDBConnectionString; + } + /// /// Get or create an instance of /// @@ -54,7 +248,7 @@ namespace Umbraco.Tests.Integration.Testing /// /// There must only be ONE instance shared between all tests in a session /// - public static LocalDbTestDatabase GetOrCreate(string filesPath, ILogger logger, IGlobalSettings globalSettings, IUmbracoDatabaseFactory dbFactory) + private static LocalDbTestDatabase GetOrCreateDatabase(string filesPath, ILogger logger, IGlobalSettings globalSettings, IUmbracoDatabaseFactory dbFactory) { lock (_dbLocker) { @@ -68,86 +262,104 @@ namespace Umbraco.Tests.Integration.Testing } } - private static readonly object _dbLocker = new object(); - private static LocalDbTestDatabase _dbInstance; - - private Action _testTeardown = null; - private Action _fixtureTeardown = null; - - public void OnTestTearDown(Action tearDown) + /// + /// Creates a LocalDb instance to use for the test + /// + /// + /// + /// + /// + /// + private void InstallTestLocalDb( + IUmbracoDatabaseFactory databaseFactory, IProfilingLogger logger, IGlobalSettings globalSettings, + IRuntimeState runtimeState, string workingDirectory, out string connectionString) { - _testTeardown = tearDown; - } + connectionString = null; + var dbFilePath = Path.Combine(workingDirectory, "LocalDb"); - public void OnFixtureTearDown(Action tearDown) - { - _fixtureTeardown = tearDown; - } - - [OneTimeTearDown] - public void FixtureTearDown() - { - _fixtureTeardown?.Invoke(); - } - - [TearDown] - public void TearDown() - { - _testTeardown?.Invoke(); - } - - [SetUp] - public async Task Setup() - { - var umbracoContainer = GetUmbracoContainer(out var serviceProviderFactory); - var testHelper = new TestHelper(); // get the currently set db options var testOptions = TestOptionAttributeBase.GetTestOptions(); - var hostBuilder = new HostBuilder() - .UseUmbraco(serviceProviderFactory) - .ConfigureServices((hostContext, services) => - { - services.AddSingleton(testHelper.DbProviderFactoryCreator); - var webHostEnvironment = testHelper.GetWebHostEnvironment(); - services.AddRequiredNetCoreServices(testHelper, webHostEnvironment); + if (testOptions.Database == UmbracoTestOptions.Database.None) + return; - // Add it! - services.AddUmbracoConfiguration(hostContext.Configuration); - services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, NoAppCache.Instance, testHelper.GetLoggingConfiguration(), out _); - services.AddUmbracoWebComponents(); - services.AddUmbracoRuntimeMinifier(hostContext.Configuration); - services.AddUmbracoBackOffice(); - services.AddUmbracoBackOfficeIdentity(); + // need to manually register this factory + DbProviderFactories.RegisterFactory(Constants.DbProviderNames.SqlServer, SqlClientFactory.Instance); - services.AddMvc(); + if (!Directory.Exists(dbFilePath)) + Directory.CreateDirectory(dbFilePath); + var db = GetOrCreateDatabase(dbFilePath, logger, globalSettings, databaseFactory); - services.AddSingleton(new ConsoleLogger(new MessageTemplates())); - - CustomTestSetup(services); - }); - - var host = await hostBuilder.StartAsync(); - var app = new ApplicationBuilder(host.Services); - Services = app.ApplicationServices; - - Services.GetRequiredService().EnsureUmbracoContext(); - // This will create a db, install the schema and ensure the app is configured to run - app.UseTestLocalDb(testHelper.WorkingDirectory, this, out var connectionString); - TestDBConnectionString = connectionString; - - if (testOptions.Boot) + switch (testOptions.Database) { - app.UseUmbracoCore(); - } + case UmbracoTestOptions.Database.NewSchemaPerTest: + // New DB + Schema + var newSchemaDbId = db.AttachSchema(); + + // Add teardown callback + OnTestTearDown(() => db.Detach(newSchemaDbId)); + + // We must re-configure our current factory since attaching a new LocalDb from the pool changes connection strings + if (!databaseFactory.Configured) + { + databaseFactory.Configure(db.ConnectionString, Constants.DatabaseProviders.SqlServer); + } + + // re-run the runtime level check + runtimeState.DetermineRuntimeLevel(); + + Assert.AreEqual(RuntimeLevel.Run, runtimeState.Level); + + break; + case UmbracoTestOptions.Database.NewEmptyPerTest: + + var newEmptyDbId = db.AttachEmpty(); + + // Add teardown callback + OnTestTearDown(() => db.Detach(newEmptyDbId)); + + + break; + case UmbracoTestOptions.Database.NewSchemaPerFixture: + + // New DB + Schema + var newSchemaFixtureDbId = db.AttachSchema(); + + // Add teardown callback + OnFixtureTearDown(() => db.Detach(newSchemaFixtureDbId)); + + break; + case UmbracoTestOptions.Database.NewEmptyPerFixture: + + throw new NotImplementedException(); + + //// Add teardown callback + //integrationTest.OnFixtureTearDown(() => db.Detach()); + + break; + default: + throw new ArgumentOutOfRangeException(nameof(testOptions), testOptions, null); + } + connectionString = db.ConnectionString; } - protected T GetRequiredService() => Services.GetRequiredService(); + #endregion #region Common services + protected LightInjectContainer UmbracoContainer { get; private set; } + private UmbracoServiceProviderFactory _serviceProviderFactory; + + protected virtual T GetRequiredService() => Services.GetRequiredService(); + + public Dictionary InMemoryConfiguration { get; } = new Dictionary(); + + public IConfiguration Configuration { get; protected set; } + + public TestHelper TestHelper = new TestHelper(); + protected string TestDBConnectionString { get; private set; } protected virtual Action CustomTestSetup => services => { }; @@ -155,7 +367,7 @@ namespace Umbraco.Tests.Integration.Testing /// /// Returns the DI container /// - protected IServiceProvider Services { get; private set; } + protected IServiceProvider Services { get; set; } /// /// Returns the diff --git a/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index 072f15bcd1..ab11207df2 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -30,6 +30,7 @@ + diff --git a/src/Umbraco.Tests/Routing/UmbracoModuleTests.cs b/src/Umbraco.Tests/Routing/UmbracoModuleTests.cs index 0165e7714f..e86c5702e3 100644 --- a/src/Umbraco.Tests/Routing/UmbracoModuleTests.cs +++ b/src/Umbraco.Tests/Routing/UmbracoModuleTests.cs @@ -32,7 +32,7 @@ namespace Umbraco.Tests.Routing //create the module var logger = Mock.Of(); var globalSettings = TestObjects.GetGlobalSettings(); - var runtime = new RuntimeState(globalSettings, UmbracoVersion); + var runtime = Umbraco.Core.RuntimeState.Booting(); _module = new UmbracoInjectedModule ( diff --git a/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs b/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs index f63c56b64e..c338cf8a50 100644 --- a/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs +++ b/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs @@ -125,7 +125,7 @@ namespace Umbraco.Tests.Runtimes // must override the database factory // else BootFailedException because U cannot connect to the configured db - protected internal override IUmbracoDatabaseFactory GetDatabaseFactory() + protected internal override IUmbracoDatabaseFactory CreateDatabaseFactory() { var mock = new Mock(); mock.Setup(x => x.Configured).Returns(true); diff --git a/src/Umbraco.Tests/Runtimes/StandaloneTests.cs b/src/Umbraco.Tests/Runtimes/StandaloneTests.cs index ac0ff3f9f9..bf26b2167c 100644 --- a/src/Umbraco.Tests/Runtimes/StandaloneTests.cs +++ b/src/Umbraco.Tests/Runtimes/StandaloneTests.cs @@ -73,7 +73,7 @@ namespace Umbraco.Tests.Runtimes var mainDom = new SimpleMainDom(); var umbracoVersion = TestHelper.GetUmbracoVersion(); var backOfficeInfo = TestHelper.GetBackOfficeInfo(); - var runtimeState = new RuntimeState(null, umbracoVersion); + var runtimeState = new RuntimeState(globalSettings, umbracoVersion, databaseFactory, logger); var configs = TestHelper.GetConfigs(); var variationContextAccessor = TestHelper.VariationContextAccessor; @@ -87,7 +87,7 @@ namespace Umbraco.Tests.Runtimes var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, typeFinder, NoAppCache.Instance); // determine actual runtime level - runtimeState.DetermineRuntimeLevel(databaseFactory, logger); + runtimeState.DetermineRuntimeLevel(); Console.WriteLine(runtimeState.Level); // going to be Install BUT we want to force components to be there (nucache etc) runtimeState.Level = RuntimeLevel.Run; diff --git a/src/Umbraco.Tests/TestHelpers/TestHelper.cs b/src/Umbraco.Tests/TestHelpers/TestHelper.cs index 9aeb668518..5c968f2f90 100644 --- a/src/Umbraco.Tests/TestHelpers/TestHelper.cs +++ b/src/Umbraco.Tests/TestHelpers/TestHelper.cs @@ -78,8 +78,6 @@ namespace Umbraco.Tests.TestHelpers public static Configs GetConfigs() => _testHelperInternal.GetConfigs(); - public static IRuntimeState GetRuntimeState() => _testHelperInternal.GetRuntimeState(); - public static IBackOfficeInfo GetBackOfficeInfo() => _testHelperInternal.GetBackOfficeInfo(); public static IConfigsFactory GetConfigsFactory() => _testHelperInternal.GetConfigsFactory(); diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeApplicationBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs similarity index 79% rename from src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeApplicationBuilderExtensions.cs rename to src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs index 282668e4e6..3cba66812e 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs @@ -7,7 +7,7 @@ using Umbraco.Web.BackOffice.Security; namespace Umbraco.Extensions { - public static class UmbracoBackOfficeApplicationBuilderExtensions + public static class BackOfficeApplicationBuilderExtensions { public static IApplicationBuilder UseUmbraco(this IApplicationBuilder app) { @@ -44,11 +44,6 @@ namespace Umbraco.Extensions app.UseUmbracoRuntimeMinification(); - // Important we handle image manipulations before the static files, otherwise the querystring is just ignored. - // TODO: Since we are dependent on these we need to register them but what happens when we call this multiple times since we are dependent on this for UseUmbracoWebsite too? - app.UseImageSharp(); - app.UseStaticFiles(); - app.UseMiddleware(); return app; diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs similarity index 64% rename from src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs rename to src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs index fc3efab5e0..f135a72293 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs @@ -20,51 +20,9 @@ using Umbraco.Web.Common.Security; namespace Umbraco.Extensions { - public static class UmbracoBackOfficeServiceCollectionExtensions + + public static class BackOfficeServiceCollectionExtensions { - public static IServiceCollection AddUmbraco(this IServiceCollection services, IWebHostEnvironment webHostEnvironment, IConfiguration config) - { - if (services == null) throw new ArgumentNullException(nameof(services)); - - // TODO: We will need to decide on if we want to use the ServiceBasedControllerActivator to create our controllers - // or use the default IControllerActivator: DefaultControllerActivator (which doesn't directly use the container to resolve controllers) - // This will affect whether we need to explicitly register controllers in the container like we do today in v8. - // What we absolutely must do though is make sure we explicitly opt-in to using one or the other *always* for our controllers instead of - // relying on a global configuration set by a user since if a custom IControllerActivator is used for our own controllers we may not - // guarantee it will work. And then... is that even possible? - - // TODO: we will need to simplify this and prob just have a one or 2 main method that devs call which call all other required methods, - // but for now we'll just be explicit with all of them - services.AddUmbracoConfiguration(config); - services.AddUmbracoCore(webHostEnvironment, out var factory); - services.AddUmbracoWebComponents(); - services.AddUmbracoRuntimeMinifier(config); - services.AddUmbracoBackOffice(); - services.AddUmbracoBackOfficeIdentity(); - services.AddMiniProfiler(options => - { - options.ShouldProfile = request => false; // WebProfiler determine and start profiling. We should not use the MiniProfilerMiddleware to also profile - }); - - //We need to have runtime compilation of views when using umbraco. We could consider having only this when a specific config is set. - //But as far as I can see, there are still precompiled views, even when this is activated, so maybe it is okay. - services.AddControllersWithViews().AddRazorRuntimeCompilation(); - - - // If using Kestrel: https://stackoverflow.com/a/55196057 - services.Configure(options => - { - options.AllowSynchronousIO = true; - }); - - services.Configure(options => - { - options.AllowSynchronousIO = true; - }); - - return services; - } - /// /// Adds the services required for running the Umbraco back office /// @@ -105,7 +63,7 @@ namespace Umbraco.Extensions // Configure the options specifically for the UmbracoBackOfficeIdentityOptions instance services.ConfigureOptions(); services.ConfigureOptions(); - } + } private static IdentityBuilder BuildUmbracoBackOfficeIdentity(this IServiceCollection services) { diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs new file mode 100644 index 0000000000..71325d70d4 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs @@ -0,0 +1,28 @@ +using Umbraco.Web.Common.Builder; + +namespace Umbraco.Extensions +{ + public static class UmbracoBuilderExtensions + { + public static void BuildWithAllBackOfficeComponents(this IUmbracoBuilder builder) + { + builder + .WithConfiguration() + .WithCore() + .WithWebComponents() + .WithRuntimeMinifier() + .WithBackOffice() + .WithBackOfficeIdentity() + .WithMiniProfiler() + .WithMvcAndRazor() + .WithWebServer() + .Build(); + } + + public static IUmbracoBuilder WithBackOffice(this IUmbracoBuilder builder) + => builder.AddWith(nameof(WithBackOffice), () => builder.Services.AddUmbracoBackOffice()); + + public static IUmbracoBuilder WithBackOfficeIdentity(this IUmbracoBuilder builder) + => builder.AddWith(nameof(WithBackOfficeIdentity), () => builder.Services.AddUmbracoBackOfficeIdentity()); + } +} diff --git a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj index 2547117531..1c66edc2ce 100644 --- a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj +++ b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/Umbraco.Web.Common/Builder/IUmbracoBuilder.cs b/src/Umbraco.Web.Common/Builder/IUmbracoBuilder.cs new file mode 100644 index 0000000000..2de7d2d285 --- /dev/null +++ b/src/Umbraco.Web.Common/Builder/IUmbracoBuilder.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Text; + +namespace Umbraco.Web.Common.Builder +{ + + public interface IUmbracoBuilder + { + IServiceCollection Services { get; } + IWebHostEnvironment WebHostEnvironment { get; } + IConfiguration Config { get; } + IUmbracoBuilder AddWith(string key, Action add); + void Build(); + } +} diff --git a/src/Umbraco.Web.Common/Builder/UmbracoBuilder.cs b/src/Umbraco.Web.Common/Builder/UmbracoBuilder.cs new file mode 100644 index 0000000000..3efb1e74f5 --- /dev/null +++ b/src/Umbraco.Web.Common/Builder/UmbracoBuilder.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; + +namespace Umbraco.Web.Common.Builder +{ + public class UmbracoBuilder : IUmbracoBuilder + { + private readonly Dictionary _registrations = new Dictionary(); + + public UmbracoBuilder(IServiceCollection services, IWebHostEnvironment webHostEnvironment, IConfiguration config) + { + Services = services; + WebHostEnvironment = webHostEnvironment; + Config = config; + } + + public IServiceCollection Services { get; } + public IWebHostEnvironment WebHostEnvironment { get; } + public IConfiguration Config { get; } + + public IUmbracoBuilder AddWith(string key, Action add) + { + if (_registrations.ContainsKey(key)) return this; + _registrations[key] = add; + return this; + } + + public void Build() + { + foreach (var a in _registrations) + a.Value(); + } + } +} diff --git a/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs new file mode 100644 index 0000000000..dd91a2cca9 --- /dev/null +++ b/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs @@ -0,0 +1,70 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System; +using Umbraco.Extensions; + +namespace Umbraco.Web.Common.Builder +{ + // TODO: We could add parameters to configure each of these for flexibility + public static class UmbracoBuilderExtensions + { + public static IUmbracoBuilder AddUmbraco(this IServiceCollection services, IWebHostEnvironment webHostEnvironment, IConfiguration config) + { + if (services is null) throw new ArgumentNullException(nameof(services)); + if (webHostEnvironment is null) throw new ArgumentNullException(nameof(webHostEnvironment)); + if (config is null) throw new ArgumentNullException(nameof(config)); + + var builder = new UmbracoBuilder(services, webHostEnvironment, config); + return builder; + } + + public static IUmbracoBuilder WithConfiguration(this IUmbracoBuilder builder) + => builder.AddWith(nameof(WithConfiguration), () => builder.Services.AddUmbracoConfiguration(builder.Config)); + + public static IUmbracoBuilder WithCore(this IUmbracoBuilder builder) + => builder.AddWith(nameof(WithCore), () => builder.Services.AddUmbracoCore(builder.WebHostEnvironment, out _)); + + public static IUmbracoBuilder WithMiniProfiler(this IUmbracoBuilder builder) + => builder.AddWith(nameof(WithMiniProfiler), () => + builder.Services.AddMiniProfiler(options => + { + options.ShouldProfile = request => false; // WebProfiler determine and start profiling. We should not use the MiniProfilerMiddleware to also profile + })); + + public static IUmbracoBuilder WithMvcAndRazor(this IUmbracoBuilder builder, Action mvcOptions = null, Action mvcBuilding = null) + => builder.AddWith(nameof(WithMvcAndRazor), () => + { + // TODO: We need to figure out if we can work around this because calling AddControllersWithViews modifies the global app and order is very important + // this will directly affect developers who need to call that themselves. + //We need to have runtime compilation of views when using umbraco. We could consider having only this when a specific config is set. + //But as far as I can see, there are still precompiled views, even when this is activated, so maybe it is okay. + var mvcBuilder = builder.Services.AddControllersWithViews(mvcOptions).AddRazorRuntimeCompilation(); + mvcBuilding?.Invoke(mvcBuilder); + }); + + public static IUmbracoBuilder WithRuntimeMinifier(this IUmbracoBuilder builder) + => builder.AddWith(nameof(WithRuntimeMinifier), () => builder.Services.AddUmbracoRuntimeMinifier(builder.Config)); + + public static IUmbracoBuilder WithWebComponents(this IUmbracoBuilder builder) + => builder.AddWith(nameof(WithWebComponents), () => builder.Services.AddUmbracoWebComponents()); + + public static IUmbracoBuilder WithWebServer(this IUmbracoBuilder builder) + => builder.AddWith(nameof(WithWebServer), () => + { + // TODO: We need to figure out why thsi is needed and fix those endpoints to not need them, we don't want to change global things + // If using Kestrel: https://stackoverflow.com/a/55196057 + builder.Services.Configure(options => + { + options.AllowSynchronousIO = true; + }); + builder.Services.Configure(options => + { + options.AllowSynchronousIO = true; + }); + }); + } +} diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs index d18275ca7f..f4ae6ed5e5 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs @@ -178,6 +178,31 @@ namespace Umbraco.Extensions IRequestCache requestCache, ILoggingConfiguration loggingConfiguration, out IFactory factory) + => services.AddUmbracoCore(webHostEnvironment, umbContainer, entryAssembly, requestCache, loggingConfiguration, GetCoreRuntime, out factory); + + /// + /// Adds the Umbraco Back Core requirements + /// + /// + /// + /// + /// + /// + /// + /// + /// Delegate to create an + /// + /// + public static IServiceCollection AddUmbracoCore( + this IServiceCollection services, + IWebHostEnvironment webHostEnvironment, + IRegister umbContainer, + Assembly entryAssembly, + IRequestCache requestCache, + ILoggingConfiguration loggingConfiguration, + // TODO: Yep that's extremely ugly + Func getRuntime, + out IFactory factory) { if (services is null) throw new ArgumentNullException(nameof(services)); var container = umbContainer; @@ -216,7 +241,7 @@ namespace Umbraco.Extensions var umbracoVersion = new UmbracoVersion(globalSettings); var typeFinder = CreateTypeFinder(logger, profiler, webHostEnvironment, entryAssembly, configs.TypeFinder()); - var coreRuntime = GetCoreRuntime( + var runtime = getRuntime( configs, umbracoVersion, ioHelper, @@ -228,7 +253,7 @@ namespace Umbraco.Extensions requestCache, dbProviderFactoryCreator); - factory = coreRuntime.Configure(container); + factory = runtime.Configure(container); return services; } diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index 2c8ce61d9a..ee657e82bb 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -22,6 +22,7 @@ + diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index 673ac02013..72e0d792f3 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Umbraco.Extensions; +using Umbraco.Web.Common.Builder; namespace Umbraco.Web.UI.NetCore { @@ -31,7 +32,8 @@ namespace Umbraco.Web.UI.NetCore // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { - services.AddUmbraco(_env, _config); + var umbracoBuilder = services.AddUmbraco(_env, _config); + umbracoBuilder.BuildWithAllBackOfficeComponents(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.