From a69739a7a261ad1c959f833e703d2aeb54567edc Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Tue, 15 Dec 2020 15:20:36 +0000 Subject: [PATCH 01/11] Remove CoreInitialComponent --- .../Events/UmbracoApplicationStarting.cs | 16 +++++ .../Events/UmbracoApplicationStopping.cs | 4 ++ src/Umbraco.Core/Services/IRuntime.cs | 11 +--- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../Runtime/CoreInitialComposer.cs | 10 +-- .../Runtime/CoreRuntime.cs | 64 ++++++++++++++----- ...ponent.cs => EssentialDirectoryCreator.cs} | 20 +++--- src/Umbraco.Tests.Integration/RuntimeTests.cs | 48 -------------- .../UmbracoTestServerTestBase.cs | 13 +--- .../Testing/UmbracoIntegrationTest.cs | 39 +++++------ .../Builder/UmbracoBuilderExtensions.cs | 4 +- .../ApplicationBuilderExtensions.cs | 48 +------------- src/Umbraco.Web/UmbracoApplicationBase.cs | 6 -- 13 files changed, 111 insertions(+), 173 deletions(-) create mode 100644 src/Umbraco.Core/Events/UmbracoApplicationStarting.cs create mode 100644 src/Umbraco.Core/Events/UmbracoApplicationStopping.cs rename src/Umbraco.Infrastructure/Runtime/{CoreInitialComponent.cs => EssentialDirectoryCreator.cs} (67%) diff --git a/src/Umbraco.Core/Events/UmbracoApplicationStarting.cs b/src/Umbraco.Core/Events/UmbracoApplicationStarting.cs new file mode 100644 index 0000000000..422673a823 --- /dev/null +++ b/src/Umbraco.Core/Events/UmbracoApplicationStarting.cs @@ -0,0 +1,16 @@ +namespace Umbraco.Core.Events +{ + public class UmbracoApplicationStarting : INotification + { + /// + /// Initializes a new instance of the class. + /// + /// The runtime level + public UmbracoApplicationStarting(RuntimeLevel runtimeLevel) => RuntimeLevel = runtimeLevel; + + /// + /// Gets the runtime level of execution. + /// + public RuntimeLevel RuntimeLevel { get; } + } +} diff --git a/src/Umbraco.Core/Events/UmbracoApplicationStopping.cs b/src/Umbraco.Core/Events/UmbracoApplicationStopping.cs new file mode 100644 index 0000000000..bef6f0de19 --- /dev/null +++ b/src/Umbraco.Core/Events/UmbracoApplicationStopping.cs @@ -0,0 +1,4 @@ +namespace Umbraco.Core.Events +{ + public class UmbracoApplicationStopping : INotification { } +} diff --git a/src/Umbraco.Core/Services/IRuntime.cs b/src/Umbraco.Core/Services/IRuntime.cs index 8a1be721d0..d1254c219f 100644 --- a/src/Umbraco.Core/Services/IRuntime.cs +++ b/src/Umbraco.Core/Services/IRuntime.cs @@ -1,22 +1,15 @@ -using System; +using Microsoft.Extensions.Hosting; namespace Umbraco.Core { /// /// Defines the Umbraco runtime. /// - public interface IRuntime + public interface IRuntime : IHostedService { /// /// Gets the runtime state. /// IRuntimeState State { get; } - - void Start(); - - /// - /// Terminates the runtime. - /// - void Terminate(); } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 300dedc1c6..24b2fae683 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -16,6 +16,7 @@ + diff --git a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs index 126d235ae0..e658405400 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs @@ -1,4 +1,4 @@ -using System; +using System; using Examine; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -38,6 +38,7 @@ using Umbraco.Core.Templates; using Umbraco.Examine; using Umbraco.Infrastructure.Examine; using Umbraco.Infrastructure.Media; +using Umbraco.Infrastructure.Runtime; using Umbraco.Web; using Umbraco.Web.Actions; using Umbraco.Web.Cache; @@ -66,11 +67,12 @@ namespace Umbraco.Core.Runtime { // core's initial composer composes before all core composers [ComposeBefore(typeof(ICoreComposer))] - public class CoreInitialComposer : ComponentComposer + public class CoreInitialComposer : IComposer { - public override void Compose(IUmbracoBuilder builder) + /// + public void Compose(IUmbracoBuilder builder) { - base.Compose(builder); + builder.AddNotificationHandler(); // composers builder diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs index cb02a90ebe..55fa3457ed 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs @@ -1,7 +1,10 @@ -using System; +using System; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Umbraco.Core; using Umbraco.Core.Composing; +using Umbraco.Core.Events; using Umbraco.Core.Hosting; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; @@ -10,71 +13,102 @@ namespace Umbraco.Infrastructure.Runtime { public class CoreRuntime : IRuntime { - public IRuntimeState State { get; } - private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; private readonly ComponentCollection _components; private readonly IApplicationShutdownRegistry _applicationShutdownRegistry; private readonly IProfilingLogger _profilingLogger; private readonly IMainDom _mainDom; private readonly IUmbracoDatabaseFactory _databaseFactory; + private readonly IEventAggregator _eventAggregator; + private readonly IHostingEnvironment _hostingEnvironment; public CoreRuntime( - ILogger logger, + ILoggerFactory loggerFactory, IRuntimeState state, ComponentCollection components, IApplicationShutdownRegistry applicationShutdownRegistry, IProfilingLogger profilingLogger, IMainDom mainDom, - IUmbracoDatabaseFactory databaseFactory) + IUmbracoDatabaseFactory databaseFactory, + IEventAggregator eventAggregator, + IHostingEnvironment hostingEnvironment) { State = state; - _logger = logger; + _loggerFactory = loggerFactory; _components = components; _applicationShutdownRegistry = applicationShutdownRegistry; _profilingLogger = profilingLogger; _mainDom = mainDom; _databaseFactory = databaseFactory; - } - + _eventAggregator = eventAggregator; + _hostingEnvironment = hostingEnvironment; - public void Start() + + _logger = _loggerFactory.CreateLogger(); + } + + /// + /// Gets the state of the Umbraco runtime. + /// + public IRuntimeState State { get; } + + /// + public async Task StartAsync(CancellationToken cancellationToken) { + StaticApplicationLogging.Initialize(_loggerFactory); + AppDomain.CurrentDomain.UnhandledException += (_, args) => { var exception = (Exception)args.ExceptionObject; var isTerminating = args.IsTerminating; // always true? var msg = "Unhandled exception in AppDomain"; - if (isTerminating) msg += " (terminating)"; + + if (isTerminating) + { + msg += " (terminating)"; + } + msg += "."; + _logger.LogError(exception, msg); }; + AppDomain.CurrentDomain.SetData("DataDirectory", _hostingEnvironment?.MapPathContentRoot(Core.Constants.SystemDirectories.Data)); + DetermineRuntimeLevel(); if (State.Level <= RuntimeLevel.BootFailed) + { throw new InvalidOperationException($"Cannot start the runtime if the runtime level is less than or equal to {RuntimeLevel.BootFailed}"); + } - var hostingEnvironmentLifetime = _applicationShutdownRegistry; + IApplicationShutdownRegistry hostingEnvironmentLifetime = _applicationShutdownRegistry; 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)}"); + { + 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(StartAsync)}"); + } // acquire the main domain - if this fails then anything that should be registered with MainDom will not operate AcquireMainDom(); + await _eventAggregator.PublishAsync(new UmbracoApplicationStarting(State.Level), cancellationToken); + // create & initialize the components _components.Initialize(); } - public void Terminate() + public async Task StopAsync(CancellationToken cancellationToken) { _components.Terminate(); + await _eventAggregator.PublishAsync(new UmbracoApplicationStopping(), cancellationToken); + StaticApplicationLogging.Initialize(null); } private void AcquireMainDom() { - using (var timer = _profilingLogger.DebugDuration("Acquiring MainDom.", "Acquired.")) + using (DisposableTimer timer = _profilingLogger.DebugDuration("Acquiring MainDom.", "Acquired.")) { try { @@ -90,7 +124,7 @@ namespace Umbraco.Infrastructure.Runtime private void DetermineRuntimeLevel() { - using var timer = _profilingLogger.DebugDuration("Determining runtime level.", "Determined."); + using DisposableTimer timer = _profilingLogger.DebugDuration("Determining runtime level.", "Determined."); try { diff --git a/src/Umbraco.Infrastructure/Runtime/CoreInitialComponent.cs b/src/Umbraco.Infrastructure/Runtime/EssentialDirectoryCreator.cs similarity index 67% rename from src/Umbraco.Infrastructure/Runtime/CoreInitialComponent.cs rename to src/Umbraco.Infrastructure/Runtime/EssentialDirectoryCreator.cs index 73318208b6..5543662464 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreInitialComponent.cs +++ b/src/Umbraco.Infrastructure/Runtime/EssentialDirectoryCreator.cs @@ -1,25 +1,28 @@ -using Microsoft.Extensions.Options; -using Umbraco.Core.Composing; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Umbraco.Core; using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Events; using Umbraco.Core.Hosting; using Umbraco.Core.IO; -namespace Umbraco.Core.Runtime +namespace Umbraco.Infrastructure.Runtime { - public class CoreInitialComponent : IComponent + public class EssentialDirectoryCreator : INotificationHandler { private readonly IIOHelper _ioHelper; private readonly IHostingEnvironment _hostingEnvironment; private readonly GlobalSettings _globalSettings; - public CoreInitialComponent(IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, IOptions globalSettings) + public EssentialDirectoryCreator(IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, IOptions globalSettings) { _ioHelper = ioHelper; _hostingEnvironment = hostingEnvironment; _globalSettings = globalSettings.Value; } - public void Initialize() + public Task HandleAsync(UmbracoApplicationStarting notification, CancellationToken cancellationToken) { // ensure we have some essential directories // every other component can then initialize safely @@ -28,9 +31,8 @@ namespace Umbraco.Core.Runtime _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MvcViews)); _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.PartialViews)); _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MacroPartials)); - } - public void Terminate() - { } + return Task.CompletedTask; + } } } diff --git a/src/Umbraco.Tests.Integration/RuntimeTests.cs b/src/Umbraco.Tests.Integration/RuntimeTests.cs index 77ee27b8c4..1e3317262a 100644 --- a/src/Umbraco.Tests.Integration/RuntimeTests.cs +++ b/src/Umbraco.Tests.Integration/RuntimeTests.cs @@ -33,54 +33,6 @@ namespace Umbraco.Tests.Integration MyComposer.Reset(); } - /// - /// Calling AddUmbracoCore to configure the container - /// - [Test] - public async Task AddUmbracoCore() - { - var testHelper = new TestHelper(); - - var hostBuilder = new HostBuilder() - .UseUmbraco() - .ConfigureServices((hostContext, services) => - { - var webHostEnvironment = testHelper.GetWebHostEnvironment(); - services.AddSingleton(testHelper.DbProviderFactoryCreator); - services.AddRequiredNetCoreServices(testHelper, webHostEnvironment); - - // Add it! - var typeLoader = services.AddTypeLoader( - GetType().Assembly, - webHostEnvironment, - testHelper.GetHostingEnvironment(), - testHelper.ConsoleLoggerFactory, - AppCaches.NoCache, - hostContext.Configuration, - testHelper.Profiler); - - var builder = new UmbracoBuilder(services, hostContext.Configuration, typeLoader, testHelper.ConsoleLoggerFactory); - builder.Services.AddUnique(AppCaches.NoCache); - builder.AddConfiguration(); - builder.AddUmbracoCore(); - }); - - var host = await hostBuilder.StartAsync(); - var app = new ApplicationBuilder(host.Services); - - // assert results - var runtimeState = app.ApplicationServices.GetRequiredService(); - var mainDom = app.ApplicationServices.GetRequiredService(); - - Assert.IsFalse(mainDom.IsMainDom); // We haven't "Started" the runtime yet - Assert.IsNull(runtimeState.BootFailedException); - Assert.IsFalse(MyComponent.IsInit); // We haven't "Started" the runtime yet - - await host.StopAsync(); - - Assert.IsFalse(MyComponent.IsTerminated); // we didn't "Start" the runtime so nothing was registered for shutdown - } - /// /// Calling AddUmbracoCore to configure the container and UseUmbracoCore to start the runtime /// diff --git a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index 93769eaaed..c3c01ed977 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -1,4 +1,4 @@ - + using System; using System.Linq.Expressions; using System.Net.Http; @@ -119,15 +119,6 @@ namespace Umbraco.Tests.Integration.TestServerTest protected LinkGenerator LinkGenerator { get; private set; } protected WebApplicationFactory Factory { get; private set; } - [TearDown] - public override void TearDown() - { - base.TearDown(); - base.TerminateCoreRuntime(); - - Factory.Dispose(); - } - #region IStartup public override void ConfigureServices(IServiceCollection services) @@ -162,7 +153,5 @@ namespace Umbraco.Tests.Integration.TestServerTest } #endregion - - } } diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index 43b2d236c7..f72139576e 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -71,9 +71,14 @@ namespace Umbraco.Tests.Integration.Testing foreach (var a in _testTeardown) a(); } + _testTeardown = null; FirstTestInFixture = false; FirstTestInSession = false; + + // Ensure CoreRuntime stopped (now it's a HostedService) + IHost host = Services.GetRequiredService(); + host.StopAsync().GetAwaiter().GetResult(); } [TearDown] @@ -100,8 +105,6 @@ namespace Umbraco.Tests.Integration.Testing var app = new ApplicationBuilder(host.Services); Configure(app); - - OnFixtureTearDown(() => host.Dispose()); } #region Generic Host Builder and Runtime @@ -135,6 +138,8 @@ namespace Umbraco.Tests.Integration.Testing /// public virtual IHostBuilder CreateHostBuilder() { + + var testOptions = TestOptionAttributeBase.GetTestOptions(); 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 @@ -154,6 +159,12 @@ namespace Umbraco.Tests.Integration.Testing { services.AddTransient(_ => CreateLoggerFactory()); ConfigureServices(services); + + if (!testOptions.Boot) + { + // If boot is false, we don't want the CoreRuntime hosted service to start + services.AddUnique(Mock.Of()); + } }); return hostBuilder; } @@ -215,27 +226,9 @@ namespace Umbraco.Tests.Integration.Testing { UseTestLocalDb(app.ApplicationServices); - //get the currently set options - var testOptions = TestOptionAttributeBase.GetTestOptions(); - if (testOptions.Boot) - { - Services.GetRequiredService().EnsureBackOfficeSecurity(); - Services.GetRequiredService().EnsureUmbracoContext(); - app.UseUmbracoCore(); // Takes 200 ms - - OnTestTearDown(TerminateCoreRuntime); - } - } - - /// - /// Some IComponents hook onto static events (e.g. Published in ContentService) - /// If these fire after the components host has been shutdown, errors can occur. - /// If CoreRuntime.Start() is called We also need to de-register the events. - /// - protected void TerminateCoreRuntime() - { - Services.GetRequiredService().Terminate(); - StaticApplicationLogging.Initialize(null); + Services.GetRequiredService().EnsureBackOfficeSecurity(); + Services.GetRequiredService().EnsureUmbracoContext(); + app.UseUmbracoCore(); } #endregion diff --git a/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs index 17c91d433a..4e528bafda 100644 --- a/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs @@ -143,11 +143,13 @@ namespace Umbraco.Core.DependencyInjection builder.Services.AddUnique(factory => factory.GetRequiredService().CreateDatabase()); builder.Services.AddUnique(factory => factory.GetRequiredService().SqlContext); builder.Services.AddUnique(); - builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddHostedService(factory => factory.GetRequiredService()); + builder.AddComposers(); return builder; diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index caf4132664..a245b8121a 100644 --- a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs @@ -1,13 +1,11 @@ using System; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Serilog.Context; using Smidge; using Smidge.Nuglify; using StackExchange.Profiling; using Umbraco.Core; -using Umbraco.Core.Hosting; using Umbraco.Infrastructure.Logging.Serilog.Enrichers; using Umbraco.Web.Common.Middleware; @@ -23,9 +21,9 @@ namespace Umbraco.Extensions /// public static bool UmbracoCanBoot(this IApplicationBuilder app) { - var runtime = app.ApplicationServices.GetRequiredService(); + var state = app.ApplicationServices.GetRequiredService(); // can't continue if boot failed - return runtime.State.Level > RuntimeLevel.BootFailed; + return state.Level > RuntimeLevel.BootFailed; } /// @@ -39,25 +37,10 @@ namespace Umbraco.Extensions if (!app.UmbracoCanBoot()) return app; - var hostingEnvironment = app.ApplicationServices.GetRequiredService(); - AppDomain.CurrentDomain.SetData("DataDirectory", hostingEnvironment?.MapPathContentRoot(Core.Constants.SystemDirectories.Data)); - - var runtime = app.ApplicationServices.GetRequiredService(); - - // Register a listener for application shutdown in order to terminate the runtime - var hostLifetime = app.ApplicationServices.GetRequiredService(); - var runtimeShutdown = new CoreRuntimeShutdown(runtime, hostLifetime); - hostLifetime.RegisterObject(runtimeShutdown); - // Register our global threadabort enricher for logging var threadAbortEnricher = app.ApplicationServices.GetRequiredService(); LogContext.Push(threadAbortEnricher); // NOTE: We are not in a using clause because we are not removing it, it is on the global context - StaticApplicationLogging.Initialize(app.ApplicationServices.GetRequiredService()); - - // Start the runtime! - runtime.Start(); - return app; } @@ -121,33 +104,6 @@ namespace Umbraco.Extensions return app; } - - /// - /// Ensures the runtime is shutdown when the application is shutting down - /// - private class CoreRuntimeShutdown : IRegisteredObject - { - public CoreRuntimeShutdown(IRuntime runtime, IApplicationShutdownRegistry hostLifetime) - { - _runtime = runtime; - _hostLifetime = hostLifetime; - } - - private bool _completed = false; - private readonly IRuntime _runtime; - private readonly IApplicationShutdownRegistry _hostLifetime; - - public void Stop(bool immediate) - { - if (!_completed) - { - _completed = true; - _runtime.Terminate(); - _hostLifetime.UnregisterObject(this); - } - - } - } } } diff --git a/src/Umbraco.Web/UmbracoApplicationBase.cs b/src/Umbraco.Web/UmbracoApplicationBase.cs index f10f4491d9..bdf5b40a02 100644 --- a/src/Umbraco.Web/UmbracoApplicationBase.cs +++ b/src/Umbraco.Web/UmbracoApplicationBase.cs @@ -46,7 +46,6 @@ namespace Umbraco.Web protected UmbracoApplicationBase() { - HostingSettings hostingSettings = null; GlobalSettings globalSettings = null; SecuritySettings securitySettings = null; @@ -60,8 +59,6 @@ namespace Umbraco.Web var backOfficeInfo = new AspNetBackOfficeInfo(globalSettings, ioHelper, _loggerFactory.CreateLogger(), Options.Create(webRoutingSettings)); var profiler = GetWebProfiler(hostingEnvironment); - StaticApplicationLogging.Initialize(_loggerFactory); - Logger = NullLogger.Instance; } private IProfiler GetWebProfiler(IHostingEnvironment hostingEnvironment) @@ -87,7 +84,6 @@ namespace Umbraco.Web _loggerFactory = loggerFactory; Logger = logger; - StaticApplicationLogging.Initialize(_loggerFactory); } protected ILogger Logger { get; } @@ -189,7 +185,6 @@ namespace Umbraco.Web LogContext.Push(new HttpRequestIdEnricher(_factory.GetRequiredService())); _runtime = _factory.GetRequiredService(); - _runtime.Start(); } // called by ASP.NET (auto event wireup) once per app domain @@ -237,7 +232,6 @@ namespace Umbraco.Web { if (_runtime != null) { - _runtime.Terminate(); _runtime.DisposeIfDisposable(); _runtime = null; From ef310920cbee4459aac0cb59319e26c448b22024 Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Tue, 15 Dec 2020 21:31:01 +0000 Subject: [PATCH 02/11] Remove ManifestWatcherComponent --- .../UmbracoBuilder.Events.cs | 18 +++++ .../Compose/ManifestWatcher.cs | 65 +++++++++++++++++++ .../Compose/ManifestWatcherComponent.cs | 51 --------------- .../Compose/ManifestWatcherComposer.cs | 16 ++++- 4 files changed, 96 insertions(+), 54 deletions(-) create mode 100644 src/Umbraco.Infrastructure/Compose/ManifestWatcher.cs delete mode 100644 src/Umbraco.Infrastructure/Compose/ManifestWatcherComponent.cs diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs index a21ae74976..a4ae2874e3 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs @@ -1,6 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Core.Events; @@ -26,5 +27,22 @@ namespace Umbraco.Core.DependencyInjection builder.Services.AddTransient(typeof(INotificationHandler), typeof(TNotificationHandler)); return builder; } + + /// + /// Registers a notification handler against the Umbraco service collection. + /// + /// The type of notification. + /// The type of notificiation handler. + /// The Umbraco builder. + /// Factory method + /// The . + public static IUmbracoBuilder AddNotificationHandler(this IUmbracoBuilder builder, Func factory) + where TNotificationHandler : class, INotificationHandler + where TNotification : INotification + { + // Register the handler as transient. This ensures that anything can be injected into it. + builder.Services.AddTransient(typeof(INotificationHandler), factory); + return builder; + } } } diff --git a/src/Umbraco.Infrastructure/Compose/ManifestWatcher.cs b/src/Umbraco.Infrastructure/Compose/ManifestWatcher.cs new file mode 100644 index 0000000000..fc18033a28 --- /dev/null +++ b/src/Umbraco.Infrastructure/Compose/ManifestWatcher.cs @@ -0,0 +1,65 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Umbraco.Core; +using Umbraco.Core.Events; +using Umbraco.Core.Hosting; +using Umbraco.Net; + +namespace Umbraco.Core.Compose +{ + public sealed class ManifestWatcher : + INotificationHandler, + INotificationHandler + { + private readonly IHostingEnvironment _hosting; + private readonly ILoggerFactory _loggerFactory; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IUmbracoApplicationLifetime _umbracoApplicationLifetime; + + // if configured and in debug mode, a ManifestWatcher watches App_Plugins folders for + // package.manifest chances and restarts the application on any change + private Core.Manifest.ManifestWatcher _mw; + + public ManifestWatcher(IHostingEnvironment hosting, ILoggerFactory loggerFactory, IHostingEnvironment hostingEnvironment, IUmbracoApplicationLifetime umbracoApplicationLifetime) + { + _hosting = hosting; + _loggerFactory = loggerFactory; + _hostingEnvironment = hostingEnvironment; + _umbracoApplicationLifetime = umbracoApplicationLifetime; + } + + public Task HandleAsync(UmbracoApplicationStarting notification, CancellationToken cancellationToken) + { + if (_hosting.IsDebugMode == false) + { + return Task.CompletedTask; + } + + var appPlugins = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.AppPlugins); + if (Directory.Exists(appPlugins) == false) + { + return Task.CompletedTask; + } + + _mw = new Core.Manifest.ManifestWatcher(_loggerFactory.CreateLogger(), _umbracoApplicationLifetime); + _mw.Start(Directory.GetDirectories(appPlugins)); + + return Task.CompletedTask; + } + + public Task HandleAsync(UmbracoApplicationStopping notification, CancellationToken cancellationToken) + { + if (_mw == null) + { + return Task.CompletedTask; + } + + _mw.Dispose(); + _mw = null; + + return Task.CompletedTask; + } + } +} diff --git a/src/Umbraco.Infrastructure/Compose/ManifestWatcherComponent.cs b/src/Umbraco.Infrastructure/Compose/ManifestWatcherComponent.cs deleted file mode 100644 index dd3a800821..0000000000 --- a/src/Umbraco.Infrastructure/Compose/ManifestWatcherComponent.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.IO; -using Microsoft.Extensions.Logging; -using Umbraco.Core.Composing; -using Umbraco.Core.Hosting; -using Umbraco.Core.Manifest; -using Umbraco.Net; - -namespace Umbraco.Core.Compose -{ - public sealed class ManifestWatcherComponent : IComponent - { - private readonly IHostingEnvironment _hosting; - private readonly ILoggerFactory _loggerFactory; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IUmbracoApplicationLifetime _umbracoApplicationLifetime; - - // if configured and in debug mode, a ManifestWatcher watches App_Plugins folders for - // package.manifest chances and restarts the application on any change - private ManifestWatcher _mw; - - public ManifestWatcherComponent(IHostingEnvironment hosting, ILoggerFactory loggerFactory, IHostingEnvironment hostingEnvironment, IUmbracoApplicationLifetime umbracoApplicationLifetime) - { - _hosting = hosting; - _loggerFactory = loggerFactory; - _hostingEnvironment = hostingEnvironment; - _umbracoApplicationLifetime = umbracoApplicationLifetime; - } - - public void Initialize() - { - if (_hosting.IsDebugMode == false) return; - - //if (ApplicationContext.Current.IsConfigured == false || GlobalSettings.DebugMode == false) - // return; - - var appPlugins = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.AppPlugins); - if (Directory.Exists(appPlugins) == false) return; - - _mw = new ManifestWatcher(_loggerFactory.CreateLogger(), _umbracoApplicationLifetime); - _mw.Start(Directory.GetDirectories(appPlugins)); - } - - public void Terminate() - { - if (_mw == null) return; - - _mw.Dispose(); - _mw = null; - } - } -} diff --git a/src/Umbraco.Infrastructure/Compose/ManifestWatcherComposer.cs b/src/Umbraco.Infrastructure/Compose/ManifestWatcherComposer.cs index 786a3ed0ce..a94106f6dc 100644 --- a/src/Umbraco.Infrastructure/Compose/ManifestWatcherComposer.cs +++ b/src/Umbraco.Infrastructure/Compose/ManifestWatcherComposer.cs @@ -1,7 +1,17 @@ -using Umbraco.Core.Composing; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Core.Composing; +using Umbraco.Core.DependencyInjection; +using Umbraco.Core.Events; namespace Umbraco.Core.Compose { - public class ManifestWatcherComposer : ComponentComposer, ICoreComposer - { } + public class ManifestWatcherComposer : ICoreComposer + { + public void Compose(IUmbracoBuilder builder) + { + builder.Services.AddSingleton(); + builder.AddNotificationHandler(factory => factory.GetRequiredService()); + builder.AddNotificationHandler(factory => factory.GetRequiredService()); + } + } } From 91e2f58822b5fac2653c8c748849807a37a0e761 Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Tue, 15 Dec 2020 21:47:15 +0000 Subject: [PATCH 03/11] Drop ManifestWatcherComposer, Convert CoreInitialComposer to extension on UmbracoBuilder --- .../Compose/ManifestWatcherComposer.cs | 17 --------------- ...tialComposer.cs => CoreInitialServices.cs} | 21 +++++++++++-------- .../{Compose => Runtime}/ManifestWatcher.cs | 2 +- .../Builder/UmbracoBuilderExtensions.cs | 1 + .../Runtime/AspNetCoreComposer.cs | 6 ------ src/Umbraco.Web/Runtime/WebInitialComposer.cs | 7 ------- 6 files changed, 14 insertions(+), 40 deletions(-) delete mode 100644 src/Umbraco.Infrastructure/Compose/ManifestWatcherComposer.cs rename src/Umbraco.Infrastructure/Runtime/{CoreInitialComposer.cs => CoreInitialServices.cs} (96%) rename src/Umbraco.Infrastructure/{Compose => Runtime}/ManifestWatcher.cs (98%) diff --git a/src/Umbraco.Infrastructure/Compose/ManifestWatcherComposer.cs b/src/Umbraco.Infrastructure/Compose/ManifestWatcherComposer.cs deleted file mode 100644 index a94106f6dc..0000000000 --- a/src/Umbraco.Infrastructure/Compose/ManifestWatcherComposer.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Umbraco.Core.Composing; -using Umbraco.Core.DependencyInjection; -using Umbraco.Core.Events; - -namespace Umbraco.Core.Compose -{ - public class ManifestWatcherComposer : ICoreComposer - { - public void Compose(IUmbracoBuilder builder) - { - builder.Services.AddSingleton(); - builder.AddNotificationHandler(factory => factory.GetRequiredService()); - builder.AddNotificationHandler(factory => factory.GetRequiredService()); - } - } -} diff --git a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs b/src/Umbraco.Infrastructure/Runtime/CoreInitialServices.cs similarity index 96% rename from src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs rename to src/Umbraco.Infrastructure/Runtime/CoreInitialServices.cs index e658405400..0fa3250522 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreInitialServices.cs @@ -3,8 +3,8 @@ using Examine; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Umbraco.Core; using Umbraco.Core.Cache; -using Umbraco.Core.Composing; using Umbraco.Core.Composing.CompositionExtensions; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Grid; @@ -38,7 +38,6 @@ using Umbraco.Core.Templates; using Umbraco.Examine; using Umbraco.Infrastructure.Examine; using Umbraco.Infrastructure.Media; -using Umbraco.Infrastructure.Runtime; using Umbraco.Web; using Umbraco.Web.Actions; using Umbraco.Web.Cache; @@ -63,17 +62,18 @@ using Umbraco.Web.Templates; using Umbraco.Web.Trees; using TextStringValueConverter = Umbraco.Core.PropertyEditors.ValueConverters.TextStringValueConverter; -namespace Umbraco.Core.Runtime +namespace Umbraco.Infrastructure.Runtime { - // core's initial composer composes before all core composers - [ComposeBefore(typeof(ICoreComposer))] - public class CoreInitialComposer : IComposer + public static class CoreInitialServices { - /// - public void Compose(IUmbracoBuilder builder) + public static IUmbracoBuilder AddCoreInitialServices(this IUmbracoBuilder builder) { builder.AddNotificationHandler(); + builder.Services.AddSingleton(); + builder.AddNotificationHandler(factory => factory.GetRequiredService()); + builder.AddNotificationHandler(factory => factory.GetRequiredService()); + // composers builder .ComposeRepositories() @@ -304,7 +304,7 @@ namespace Umbraco.Core.Runtime // register *all* checks, except those marked [HideFromTypeFinder] of course builder.Services.AddUnique(); builder.HealthChecks() - .Add(() => builder.TypeLoader.GetTypes()); + .Add(() => builder.TypeLoader.GetTypes()); builder.WithCollectionBuilder() .Add(() => builder.TypeLoader.GetTypes()); @@ -384,7 +384,10 @@ namespace Umbraco.Core.Runtime builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); + + return builder; } } } diff --git a/src/Umbraco.Infrastructure/Compose/ManifestWatcher.cs b/src/Umbraco.Infrastructure/Runtime/ManifestWatcher.cs similarity index 98% rename from src/Umbraco.Infrastructure/Compose/ManifestWatcher.cs rename to src/Umbraco.Infrastructure/Runtime/ManifestWatcher.cs index fc18033a28..358dfb2ed5 100644 --- a/src/Umbraco.Infrastructure/Compose/ManifestWatcher.cs +++ b/src/Umbraco.Infrastructure/Runtime/ManifestWatcher.cs @@ -7,7 +7,7 @@ using Umbraco.Core.Events; using Umbraco.Core.Hosting; using Umbraco.Net; -namespace Umbraco.Core.Compose +namespace Umbraco.Infrastructure.Runtime { public sealed class ManifestWatcher : INotificationHandler, diff --git a/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs index 4e528bafda..9ecda715ee 100644 --- a/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs @@ -150,6 +150,7 @@ namespace Umbraco.Core.DependencyInjection builder.Services.AddUnique(); builder.Services.AddHostedService(factory => factory.GetRequiredService()); + builder.AddCoreInitialServices(); builder.AddComposers(); return builder; diff --git a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs index f6849215df..508f4e987f 100644 --- a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs +++ b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs @@ -1,22 +1,17 @@ using System.Linq; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; using Umbraco.Core; using Umbraco.Core.DependencyInjection; using Umbraco.Core.Composing; -using Umbraco.Core.Configuration.Models; using Umbraco.Core.Diagnostics; using Umbraco.Core.Hosting; using Umbraco.Core.Logging; -using Umbraco.Core.Runtime; using Umbraco.Core.Security; -using Umbraco.Core.Services; using Umbraco.Extensions; using Umbraco.Net; using Umbraco.Web.Common.AspNetCore; using Umbraco.Web.Common.Controllers; -using Umbraco.Web.Common.Formatters; using Umbraco.Web.Common.Install; using Umbraco.Web.Common.Lifetime; using Umbraco.Web.Common.Macros; @@ -38,7 +33,6 @@ namespace Umbraco.Web.Common.Runtime /// Adds/replaces AspNetCore specific services /// [ComposeBefore(typeof(ICoreComposer))] - [ComposeAfter(typeof(CoreInitialComposer))] public class AspNetCoreComposer : ComponentComposer, IComposer { public override void Compose(IUmbracoBuilder builder) diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 49cff7196e..7528755865 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -1,26 +1,19 @@ -using System.Web.Mvc; using System.Web.Security; -using Microsoft.AspNet.SignalR; using Microsoft.Extensions.DependencyInjection; using Umbraco.Core; using Umbraco.Core.DependencyInjection; using Umbraco.Core.Composing; using Umbraco.Core.Dictionary; using Umbraco.Core.Templates; -using Umbraco.Core.Runtime; using Umbraco.Core.Security; using Umbraco.Core.Services; -using Umbraco.Web.Composing.CompositionExtensions; using Umbraco.Web.Macros; -using Umbraco.Web.Mvc; using Umbraco.Web.PublishedCache; using Umbraco.Web.Security; using Umbraco.Web.Security.Providers; namespace Umbraco.Web.Runtime { - // web's initial composer composes after core's, and before all core composers - [ComposeAfter(typeof(CoreInitialComposer))] [ComposeBefore(typeof(ICoreComposer))] public sealed class WebInitialComposer : ComponentComposer { From 431403e372d34aab4c12293ebe8125be22ff3a67 Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Wed, 16 Dec 2020 01:54:49 +0000 Subject: [PATCH 04/11] Fixup CoreRuntime so it starts after Startup.Configure Makes integration tests play nice with Components as RuntimeLevel will be correct --- .gitignore | 1 + .../Composing/HostBuilderExtensions.cs | 20 ++++-- .../PublishedSnapshotService.cs | 10 +-- src/Umbraco.Tests.Integration/RuntimeTests.cs | 11 ++-- .../UmbracoTestServerTestBase.cs | 13 ++-- .../Testing/TestDatabaseFactory.cs | 25 +++++--- .../Testing/UmbracoIntegrationTest.cs | 64 +++++++++---------- .../Builder/UmbracoBuilderExtensions.cs | 2 - src/Umbraco.Web.UI.NetCore/Program.cs | 4 +- 9 files changed, 82 insertions(+), 68 deletions(-) diff --git a/.gitignore b/.gitignore index 022acb5db7..ea2ddfbb68 100644 --- a/.gitignore +++ b/.gitignore @@ -191,6 +191,7 @@ src/Umbraco.Web.UI/Umbraco/telemetrics-id.umb /src/Umbraco.Web.UI.NetCore/App_Data/Smidge/Cache/* /src/Umbraco.Web.UI.NetCore/umbraco/logs +src/Umbraco.Tests.Integration/umbraco/Data/ src/Umbraco.Tests.Integration/umbraco/logs/ src/Umbraco.Tests.Integration/Views/ diff --git a/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs b/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs index 7c17a8a338..e2be3569d6 100644 --- a/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs +++ b/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs @@ -1,19 +1,29 @@ -using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; namespace Umbraco.Core.Composing { /// - /// Extends the to enable Umbraco to be used as the service container. + /// Extends the to add CoreRuntime as a HostedService /// public static class HostBuilderExtensions { /// - /// Assigns a custom service provider factory to use Umbraco's container + /// Adds CoreRuntime as HostedService /// - /// - /// + /// + /// When running the site should be called before ConfigureWebDefaults. + /// + /// + /// When testing should be called after ConfigureWebDefaults to ensure UseTestDatabase is called before CoreRuntime + /// starts or we initialize components with incorrect run level. + /// + /// public static IHostBuilder UseUmbraco(this IHostBuilder builder) { + _ = builder.ConfigureServices((context, services) => + services.AddSingleton(factory => factory.GetRequiredService())); + return builder; } } diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs index 97e3df16a6..82c2fa8dd1 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs @@ -135,16 +135,16 @@ namespace Umbraco.Web.PublishedCache.NuCache _entitySerializer = entitySerializer; _publishedModelFactory = publishedModelFactory; - // we always want to handle repository events, configured or not - // assuming no repository event will trigger before the whole db is ready - // (ideally we'd have Upgrading.App vs Upgrading.Data application states...) - InitializeRepositoryEvents(); - _lifeTime.ApplicationInit += OnApplicationInit; } internal void OnApplicationInit(object sender, EventArgs e) { + // we always want to handle repository events, configured or not + // assuming no repository event will trigger before the whole db is ready + // (ideally we'd have Upgrading.App vs Upgrading.Data application states...) + InitializeRepositoryEvents(); + // however, the cache is NOT available until we are configured, because loading // content (and content types) from database cannot be consistent (see notes in "Handle // Notifications" region), so diff --git a/src/Umbraco.Tests.Integration/RuntimeTests.cs b/src/Umbraco.Tests.Integration/RuntimeTests.cs index 1e3317262a..5d721ad176 100644 --- a/src/Umbraco.Tests.Integration/RuntimeTests.cs +++ b/src/Umbraco.Tests.Integration/RuntimeTests.cs @@ -43,7 +43,6 @@ namespace Umbraco.Tests.Integration var testHelper = new TestHelper(); var hostBuilder = new HostBuilder() - .UseUmbraco() .ConfigureServices((hostContext, services) => { var webHostEnvironment = testHelper.GetWebHostEnvironment(); @@ -61,14 +60,16 @@ namespace Umbraco.Tests.Integration hostContext.Configuration, testHelper.Profiler); - var builder = new UmbracoBuilder(services, hostContext.Configuration, typeLoader, testHelper.ConsoleLoggerFactory); + var builder = new UmbracoBuilder(services, hostContext.Configuration, typeLoader, + testHelper.ConsoleLoggerFactory); builder.Services.AddUnique(AppCaches.NoCache); builder.AddConfiguration() - .AddUmbracoCore() - .Build(); + .AddUmbracoCore() + .Build(); services.AddRouting(); // LinkGenerator - }); + }) + .UseUmbraco(); var host = await hostBuilder.StartAsync(); var app = new ApplicationBuilder(host.Services); diff --git a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index c3c01ed977..87ac4505e5 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -1,8 +1,6 @@ - using System; using System.Linq.Expressions; using System.Net.Http; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -11,7 +9,6 @@ using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using NUnit.Framework; using Umbraco.Core; using Umbraco.Extensions; @@ -22,8 +19,7 @@ using Umbraco.Core.DependencyInjection; using Umbraco.Web.Common.Controllers; using Microsoft.Extensions.Hosting; using Umbraco.Core.Cache; -using Umbraco.Core.Persistence; -using Umbraco.Core.Runtime; +using Umbraco.Core.Composing; using Umbraco.Web.BackOffice.Controllers; namespace Umbraco.Tests.Integration.TestServerTest @@ -75,11 +71,14 @@ namespace Umbraco.Tests.Integration.TestServerTest // call startup builder.Configure(app => { - UseTestLocalDb(app.ApplicationServices); + UseTestDatabase(app.ApplicationServices); Services = app.ApplicationServices; Configure(app); }); - }).UseEnvironment(Environments.Development); + + }).UseEnvironment(Environments.Development); + + builder.UseUmbraco(); // Ensures CoreRuntime.StartAsync is called, must be after ConfigureWebHost return builder; } diff --git a/src/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs b/src/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs index 9bcbfa4d3a..60c767e17e 100644 --- a/src/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs +++ b/src/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using Microsoft.Extensions.Logging; using Umbraco.Core.Persistence; @@ -8,13 +9,20 @@ namespace Umbraco.Tests.Integration.Testing { public static ITestDatabase Create(string filesPath, ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory) { - return string.IsNullOrEmpty(Environment.GetEnvironmentVariable("UmbracoIntegrationTestConnectionString")) - ? CreateLocalDb(filesPath, loggerFactory, dbFactory.Create()) - : CreateSqlDeveloper(loggerFactory, dbFactory.Create()); + var connectionString = Environment.GetEnvironmentVariable("UmbracoIntegrationTestConnectionString"); + + return string.IsNullOrEmpty(connectionString) + ? CreateLocalDb(filesPath, loggerFactory, dbFactory) + : CreateSqlDeveloper(loggerFactory, dbFactory, connectionString); } - private static ITestDatabase CreateLocalDb(string filesPath, ILoggerFactory loggerFactory, IUmbracoDatabaseFactory dbFactory) + private static ITestDatabase CreateLocalDb(string filesPath, ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory) { + if (!Directory.Exists(filesPath)) + { + Directory.CreateDirectory(filesPath); + } + var localDb = new LocalDb(); if (!localDb.IsAvailable) @@ -22,22 +30,21 @@ namespace Umbraco.Tests.Integration.Testing throw new InvalidOperationException("LocalDB is not available."); } - return new LocalDbTestDatabase(loggerFactory, localDb, filesPath, dbFactory); + return new LocalDbTestDatabase(loggerFactory, localDb, filesPath, dbFactory.Create()); } - private static ITestDatabase CreateSqlDeveloper(ILoggerFactory loggerFactory, IUmbracoDatabaseFactory dbFactory) + private static ITestDatabase CreateSqlDeveloper(ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory, string connectionString) { + // NOTE: Example setup for Linux box. // $ export SA_PASSWORD=Foobar123! // $ export UmbracoIntegrationTestConnectionString="Server=localhost,1433;User Id=sa;Password=$SA_PASSWORD;" // $ docker run -e 'ACCEPT_EULA=Y' -e "SA_PASSWORD=$SA_PASSWORD" -e 'MSSQL_PID=Developer' -p 1433:1433 -d mcr.microsoft.com/mssql/server:2017-latest-ubuntu - var connectionString = Environment.GetEnvironmentVariable("UmbracoIntegrationTestConnectionString"); - if (string.IsNullOrEmpty(connectionString)) { throw new InvalidOperationException("ENV: UmbracoIntegrationTestConnectionString is not set"); } - return new SqlDeveloperTestDatabase(loggerFactory, dbFactory, connectionString); + return new SqlDeveloperTestDatabase(loggerFactory, dbFactory.Create(), connectionString); } } } diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index f72139576e..5fb3c50cc8 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -97,10 +97,10 @@ namespace Umbraco.Tests.Integration.Testing public virtual void Setup() { InMemoryConfiguration[Constants.Configuration.ConfigGlobal + ":" + nameof(GlobalSettings.InstallEmptyDatabase)] = "true"; - var hostBuilder = CreateHostBuilder(); + var hostBuilder = CreateHostBuilder() + .UseUmbraco(); // This ensures CoreRuntime.StartAsync will be called (however it's a mock if boot = false) var host = hostBuilder.Start(); - Services = host.Services; var app = new ApplicationBuilder(host.Services); @@ -113,8 +113,7 @@ namespace Umbraco.Tests.Integration.Testing { try { - var testOptions = TestOptionAttributeBase.GetTestOptions(); - switch (testOptions.Logger) + switch (TestOptions.Logger) { case UmbracoTestOptions.Logger.Mock: return NullLoggerFactory.Instance; @@ -138,15 +137,13 @@ namespace Umbraco.Tests.Integration.Testing /// public virtual IHostBuilder CreateHostBuilder() { - - var testOptions = TestOptionAttributeBase.GetTestOptions(); 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() + .ConfigureAppConfiguration((context, configBuilder) => { context.HostingEnvironment = TestHelper.GetWebHostEnvironment(); @@ -157,12 +154,13 @@ namespace Umbraco.Tests.Integration.Testing }) .ConfigureServices((hostContext, services) => { - services.AddTransient(_ => CreateLoggerFactory()); ConfigureServices(services); + services.AddUnique(CreateLoggerFactory()); - if (!testOptions.Boot) + if (!TestOptions.Boot) { // If boot is false, we don't want the CoreRuntime hosted service to start + // So we replace it with a Mock services.AddUnique(Mock.Of()); } }); @@ -224,11 +222,15 @@ namespace Umbraco.Tests.Integration.Testing public virtual void Configure(IApplicationBuilder app) { - UseTestLocalDb(app.ApplicationServices); + UseTestDatabase(app.ApplicationServices); - Services.GetRequiredService().EnsureBackOfficeSecurity(); - Services.GetRequiredService().EnsureUmbracoContext(); - app.UseUmbracoCore(); + if (TestOptions.Boot) + { + Services.GetRequiredService().EnsureBackOfficeSecurity(); + Services.GetRequiredService().EnsureUmbracoContext(); + } + + app.UseUmbracoCore(); // This no longer starts CoreRuntime, it's very fast } #endregion @@ -239,25 +241,20 @@ namespace Umbraco.Tests.Integration.Testing private static ITestDatabase _dbInstance; private static TestDbMeta _fixtureDbMeta; - protected void UseTestLocalDb(IServiceProvider serviceProvider) + protected void UseTestDatabase(IServiceProvider serviceProvider) { var state = serviceProvider.GetRequiredService(); var testDatabaseFactoryProvider = serviceProvider.GetRequiredService(); var databaseFactory = serviceProvider.GetRequiredService(); + var loggerFactory = serviceProvider.GetRequiredService(); // This will create a db, install the schema and ensure the app is configured to run - InstallTestLocalDb(testDatabaseFactoryProvider, databaseFactory, serviceProvider.GetRequiredService(), state, TestHelper.WorkingDirectory); + SetupTestDatabase(testDatabaseFactoryProvider, databaseFactory, loggerFactory, state, TestHelper.WorkingDirectory); } /// - /// Get or create an instance of + /// Get or create an instance of /// - /// - /// - /// - /// - /// - /// /// /// There must only be ONE instance shared between all tests in a session /// @@ -266,9 +263,12 @@ namespace Umbraco.Tests.Integration.Testing lock (_dbLocker) { if (_dbInstance != null) + { return _dbInstance; + } _dbInstance = TestDatabaseFactory.Create(filesPath, loggerFactory, dbFactory); + return _dbInstance; } } @@ -276,30 +276,26 @@ namespace Umbraco.Tests.Integration.Testing /// /// Creates a LocalDb instance to use for the test /// - private void InstallTestLocalDb( + private void SetupTestDatabase( TestUmbracoDatabaseFactoryProvider testUmbracoDatabaseFactoryProvider, IUmbracoDatabaseFactory databaseFactory, ILoggerFactory loggerFactory, IRuntimeState runtimeState, string workingDirectory) { - var dbFilePath = Path.Combine(workingDirectory, "LocalDb"); - - // get the currently set db options - var testOptions = TestOptionAttributeBase.GetTestOptions(); - - if (testOptions.Database == UmbracoTestOptions.Database.None) + if (TestOptions.Database == UmbracoTestOptions.Database.None) + { return; + } // need to manually register this factory DbProviderFactories.RegisterFactory(Constants.DbProviderNames.SqlServer, SqlClientFactory.Instance); - if (!Directory.Exists(dbFilePath)) - Directory.CreateDirectory(dbFilePath); + var dbFilePath = Path.Combine(workingDirectory, "LocalDb"); var db = GetOrCreateDatabase(dbFilePath, loggerFactory, testUmbracoDatabaseFactoryProvider); - switch (testOptions.Database) + switch (TestOptions.Database) { case UmbracoTestOptions.Database.NewSchemaPerTest: @@ -385,7 +381,7 @@ namespace Umbraco.Tests.Integration.Testing break; default: - throw new ArgumentOutOfRangeException(nameof(testOptions), testOptions, null); + throw new ArgumentOutOfRangeException(nameof(TestOptions), TestOptions, null); } } @@ -393,6 +389,8 @@ namespace Umbraco.Tests.Integration.Testing #region Common services + protected UmbracoTestAttribute TestOptions => TestOptionAttributeBase.GetTestOptions(); + protected virtual T GetRequiredService() => Services.GetRequiredService(); public Dictionary InMemoryConfiguration { get; } = new Dictionary(); diff --git a/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs index 9ecda715ee..b8dccfbe33 100644 --- a/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs @@ -146,9 +146,7 @@ namespace Umbraco.Core.DependencyInjection builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddHostedService(factory => factory.GetRequiredService()); builder.AddCoreInitialServices(); builder.AddComposers(); diff --git a/src/Umbraco.Web.UI.NetCore/Program.cs b/src/Umbraco.Web.UI.NetCore/Program.cs index 4a7722597d..95322cb1b0 100644 --- a/src/Umbraco.Web.UI.NetCore/Program.cs +++ b/src/Umbraco.Web.UI.NetCore/Program.cs @@ -20,7 +20,7 @@ namespace Umbraco.Web.UI.NetCore { x.ClearProviders(); }) - .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }) - .UseUmbraco(); + .UseUmbraco() + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); } } From 6dd5e04a2be26e82ed8df8227e86a819fdd556f4 Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Thu, 17 Dec 2020 11:12:13 +0000 Subject: [PATCH 05/11] Fix broken integration test, must set ServiceProvider before using --- .../TestServerTest/UmbracoTestServerTestBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index 87ac4505e5..8e6c02ca15 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -71,8 +71,8 @@ namespace Umbraco.Tests.Integration.TestServerTest // call startup builder.Configure(app => { - UseTestDatabase(app.ApplicationServices); Services = app.ApplicationServices; + UseTestDatabase(app.ApplicationServices); Configure(app); }); From 79aa2e1cc24dad6d72548811a8d8317a55c1a0dc Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Thu, 17 Dec 2020 11:15:58 +0000 Subject: [PATCH 06/11] LocalDb TestDatabase cleanup LocalDbTestDatabase now renames the final database like it used to (guid vs sensible name) Made serilog log to integration tests TEMP as far as I can tell it wasn't writing at all before --- .../Testing/BaseTestDatabase.cs | 4 +- .../Testing/LocalDbTestDatabase.cs | 14 +++- .../Testing/UmbracoIntegrationTest.cs | 70 +++++++++---------- 3 files changed, 48 insertions(+), 40 deletions(-) diff --git a/src/Umbraco.Tests.Integration/Testing/BaseTestDatabase.cs b/src/Umbraco.Tests.Integration/Testing/BaseTestDatabase.cs index 02a3da676a..2a9733fd9d 100644 --- a/src/Umbraco.Tests.Integration/Testing/BaseTestDatabase.cs +++ b/src/Umbraco.Tests.Integration/Testing/BaseTestDatabase.cs @@ -17,8 +17,8 @@ namespace Umbraco.Tests.Integration.Testing { protected ILoggerFactory _loggerFactory; protected IUmbracoDatabaseFactory _databaseFactory; - protected IEnumerable _testDatabases; - + protected IList _testDatabases; + protected UmbracoDatabase.CommandInfo[] _cachedDatabaseInitCommands; protected BlockingCollection _prepareQueue; diff --git a/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs b/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs index a9a842cdcd..add3df9fe8 100644 --- a/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs +++ b/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs @@ -64,13 +64,17 @@ namespace Umbraco.Tests.Integration.Testing var tempName = Guid.NewGuid().ToString("N"); _localDbInstance.CreateDatabase(tempName, _filesPath); _localDbInstance.DetachDatabase(tempName); + _prepareQueue = new BlockingCollection(); _readySchemaQueue = new BlockingCollection(); _readyEmptyQueue = new BlockingCollection(); - foreach (var meta in _testDatabases) + for (var i = 0; i < _testDatabases.Count; i++) { - _localDb.CopyDatabaseFiles(tempName, _filesPath, targetDatabaseName: meta.Name, overwrite: true, delete: false); + var meta = _testDatabases[i]; + var isLast = i == _testDatabases.Count - 1; + + _localDb.CopyDatabaseFiles(tempName, _filesPath, targetDatabaseName: meta.Name, overwrite: true, delete: isLast); meta.ConnectionString = _localDbInstance.GetAttachedConnectionString(meta.Name, _filesPath); _prepareQueue.Add(meta); } @@ -85,7 +89,9 @@ namespace Umbraco.Tests.Integration.Testing public void Finish() { if (_prepareQueue == null) + { return; + } _prepareQueue.CompleteAdding(); while (_prepareQueue.TryTake(out _)) @@ -100,14 +106,18 @@ namespace Umbraco.Tests.Integration.Testing { } if (_filesPath == null) + { return; + } var filename = Path.Combine(_filesPath, DatabaseName).ToUpper(); foreach (var database in _localDbInstance.GetDatabases()) { if (database.StartsWith(filename)) + { _localDbInstance.DropDatabase(database); + } } foreach (var file in Directory.EnumerateFiles(_filesPath)) diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index 5fb3c50cc8..52b74f6bbb 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -100,8 +100,10 @@ namespace Umbraco.Tests.Integration.Testing var hostBuilder = CreateHostBuilder() .UseUmbraco(); // This ensures CoreRuntime.StartAsync will be called (however it's a mock if boot = false) - var host = hostBuilder.Start(); + IHost host = hostBuilder.Build(); Services = host.Services; + host.Start(); + var app = new ApplicationBuilder(host.Services); Configure(app); @@ -118,7 +120,16 @@ namespace Umbraco.Tests.Integration.Testing case UmbracoTestOptions.Logger.Mock: return NullLoggerFactory.Instance; case UmbracoTestOptions.Logger.Serilog: - return Microsoft.Extensions.Logging.LoggerFactory.Create(builder => { builder.AddSerilog(); }); + return Microsoft.Extensions.Logging.LoggerFactory.Create(builder => + { + var path = Path.Combine(TestHelper.WorkingDirectory, "logs", "umbraco_integration_tests_.txt"); + + Log.Logger = new LoggerConfiguration() + .WriteTo.File(path, rollingInterval: RollingInterval.Day) + .CreateLogger(); + + builder.AddSerilog(Log.Logger); + }); case UmbracoTestOptions.Logger.Console: return Microsoft.Extensions.Logging.LoggerFactory.Create(builder => { builder.AddConsole(); }); } @@ -300,37 +311,23 @@ namespace Umbraco.Tests.Integration.Testing case UmbracoTestOptions.Database.NewSchemaPerTest: // New DB + Schema - var newSchemaDbMeta = db.AttachSchema(); + TestDbMeta newSchemaDbMeta = db.AttachSchema(); // Add teardown callback OnTestTearDown(() => db.Detach(newSchemaDbMeta)); - // We must re-configure our current factory since attaching a new LocalDb from the pool changes connection strings - if (!databaseFactory.Configured) - { - databaseFactory.Configure(newSchemaDbMeta.ConnectionString, Constants.DatabaseProviders.SqlServer); - } - - // re-run the runtime level check - runtimeState.DetermineRuntimeLevel(); + ConfigureTestDatabaseFactory(newSchemaDbMeta, databaseFactory, runtimeState); Assert.AreEqual(RuntimeLevel.Run, runtimeState.Level); break; case UmbracoTestOptions.Database.NewEmptyPerTest: - var newEmptyDbMeta = db.AttachEmpty(); + TestDbMeta newEmptyDbMeta = db.AttachEmpty(); // Add teardown callback OnTestTearDown(() => db.Detach(newEmptyDbMeta)); - // We must re-configure our current factory since attaching a new LocalDb from the pool changes connection strings - if (!databaseFactory.Configured) - { - databaseFactory.Configure(newEmptyDbMeta.ConnectionString, Constants.DatabaseProviders.SqlServer); - } - - // re-run the runtime level check - runtimeState.DetermineRuntimeLevel(); + ConfigureTestDatabaseFactory(newEmptyDbMeta, databaseFactory, runtimeState); Assert.AreEqual(RuntimeLevel.Install, runtimeState.Level); @@ -342,21 +339,14 @@ namespace Umbraco.Tests.Integration.Testing if (FirstTestInFixture) { // New DB + Schema - var newSchemaFixtureDbMeta = db.AttachSchema(); + TestDbMeta newSchemaFixtureDbMeta = db.AttachSchema(); _fixtureDbMeta = newSchemaFixtureDbMeta; // Add teardown callback OnFixtureTearDown(() => db.Detach(newSchemaFixtureDbMeta)); } - // We must re-configure our current factory since attaching a new LocalDb from the pool changes connection strings - if (!databaseFactory.Configured) - { - databaseFactory.Configure(_fixtureDbMeta.ConnectionString, Constants.DatabaseProviders.SqlServer); - } - - // re-run the runtime level check - runtimeState.DetermineRuntimeLevel(); + ConfigureTestDatabaseFactory(_fixtureDbMeta, databaseFactory, runtimeState); break; case UmbracoTestOptions.Database.NewEmptyPerFixture: @@ -366,18 +356,14 @@ namespace Umbraco.Tests.Integration.Testing if (FirstTestInFixture) { // New DB + Schema - var newEmptyFixtureDbMeta = db.AttachEmpty(); + TestDbMeta newEmptyFixtureDbMeta = db.AttachEmpty(); _fixtureDbMeta = newEmptyFixtureDbMeta; // Add teardown callback OnFixtureTearDown(() => db.Detach(newEmptyFixtureDbMeta)); } - // We must re-configure our current factory since attaching a new LocalDb from the pool changes connection strings - if (!databaseFactory.Configured) - { - databaseFactory.Configure(_fixtureDbMeta.ConnectionString, Constants.DatabaseProviders.SqlServer); - } + ConfigureTestDatabaseFactory(_fixtureDbMeta, databaseFactory, runtimeState); break; default: @@ -385,6 +371,19 @@ namespace Umbraco.Tests.Integration.Testing } } + private void ConfigureTestDatabaseFactory(TestDbMeta meta, IUmbracoDatabaseFactory factory, IRuntimeState state) + { + ILogger log = Services.GetRequiredService>(); + log.LogInformation($"ConfigureTestDatabaseFactory - Using test database: [{meta.Name}] - IsEmpty: [{meta.IsEmpty}]"); + + // It's just been pulled from container and wasn't used to create test database + Assert.IsFalse(factory.Configured); + + factory.Configure(meta.ConnectionString, Constants.DatabaseProviders.SqlServer); + state.DetermineRuntimeLevel(); + log.LogInformation($"ConfigureTestDatabaseFactory - Determined RuntimeLevel: [{state.Level}]"); + } + #endregion #region Common services @@ -420,7 +419,6 @@ namespace Umbraco.Tests.Integration.Testing /// Returns the /// protected ILoggerFactory LoggerFactory => Services.GetRequiredService(); - protected AppCaches AppCaches => Services.GetRequiredService(); protected IIOHelper IOHelper => Services.GetRequiredService(); protected IShortStringHelper ShortStringHelper => Services.GetRequiredService(); From a883df9d882ec064cf65c15453499cb102036cbf Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Thu, 17 Dec 2020 12:18:28 +0000 Subject: [PATCH 07/11] Use HostBuilder.UseUmbraco in same order for tests and running web app --- .../Composing/HostBuilderExtensions.cs | 7 +------ src/Umbraco.Tests.Integration/RuntimeTests.cs | 4 ++-- .../UmbracoTestServerTestBase.cs | 7 +------ .../UmbracoWebApplicationFactory.cs | 18 ++++++++++++++++-- .../Testing/UmbracoIntegrationTest.cs | 16 +++++++++------- 5 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs b/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs index e2be3569d6..8e1176747c 100644 --- a/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs +++ b/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs @@ -12,12 +12,7 @@ namespace Umbraco.Core.Composing /// Adds CoreRuntime as HostedService /// /// - /// When running the site should be called before ConfigureWebDefaults. - /// - /// - /// When testing should be called after ConfigureWebDefaults to ensure UseTestDatabase is called before CoreRuntime - /// starts or we initialize components with incorrect run level. - /// + /// Should be called before ConfigureWebDefaults. /// public static IHostBuilder UseUmbraco(this IHostBuilder builder) { diff --git a/src/Umbraco.Tests.Integration/RuntimeTests.cs b/src/Umbraco.Tests.Integration/RuntimeTests.cs index 5d721ad176..cd6909a8c5 100644 --- a/src/Umbraco.Tests.Integration/RuntimeTests.cs +++ b/src/Umbraco.Tests.Integration/RuntimeTests.cs @@ -43,6 +43,7 @@ namespace Umbraco.Tests.Integration var testHelper = new TestHelper(); var hostBuilder = new HostBuilder() + .UseUmbraco() .ConfigureServices((hostContext, services) => { var webHostEnvironment = testHelper.GetWebHostEnvironment(); @@ -68,8 +69,7 @@ namespace Umbraco.Tests.Integration .Build(); services.AddRouting(); // LinkGenerator - }) - .UseUmbraco(); + }); var host = await hostBuilder.StartAsync(); var app = new ApplicationBuilder(host.Services); diff --git a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index 8e6c02ca15..2b7aea8189 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -19,7 +19,6 @@ using Umbraco.Core.DependencyInjection; using Umbraco.Web.Common.Controllers; using Microsoft.Extensions.Hosting; using Umbraco.Core.Cache; -using Umbraco.Core.Composing; using Umbraco.Web.BackOffice.Controllers; namespace Umbraco.Tests.Integration.TestServerTest @@ -35,7 +34,7 @@ namespace Umbraco.Tests.Integration.TestServerTest InMemoryConfiguration["Umbraco:CMS:Hosting:Debug"] = "true"; // create new WebApplicationFactory specifying 'this' as the IStartup instance - var factory = new UmbracoWebApplicationFactory(CreateHostBuilder); + var factory = new UmbracoWebApplicationFactory(CreateHostBuilder, BeforeHostStart); // additional host configuration for web server integration tests Factory = factory.WithWebHostBuilder(builder => @@ -71,15 +70,11 @@ namespace Umbraco.Tests.Integration.TestServerTest // call startup builder.Configure(app => { - Services = app.ApplicationServices; - UseTestDatabase(app.ApplicationServices); Configure(app); }); }).UseEnvironment(Environments.Development); - builder.UseUmbraco(); // Ensures CoreRuntime.StartAsync is called, must be after ConfigureWebHost - return builder; } diff --git a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoWebApplicationFactory.cs b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoWebApplicationFactory.cs index 251f155d2c..256c5c59a9 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoWebApplicationFactory.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoWebApplicationFactory.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Hosting; @@ -8,16 +8,30 @@ namespace Umbraco.Tests.Integration.TestServerTest public class UmbracoWebApplicationFactory : WebApplicationFactory where TStartup : class { private readonly Func _createHostBuilder; + private readonly Action _beforeStart; /// /// Constructor to create a new WebApplicationFactory /// /// Method to create the IHostBuilder - public UmbracoWebApplicationFactory(Func createHostBuilder) + /// Method to perform an action before IHost starts + public UmbracoWebApplicationFactory(Func createHostBuilder, Action beforeStart = null) { _createHostBuilder = createHostBuilder; + _beforeStart = beforeStart; } protected override IHostBuilder CreateHostBuilder() => _createHostBuilder(); + + protected override IHost CreateHost(IHostBuilder builder) + { + IHost host = builder.Build(); + + _beforeStart?.Invoke(host); + + host.Start(); + + return host; + } } } diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index 52b74f6bbb..e62b3f5c7f 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -97,18 +97,22 @@ namespace Umbraco.Tests.Integration.Testing public virtual void Setup() { InMemoryConfiguration[Constants.Configuration.ConfigGlobal + ":" + nameof(GlobalSettings.InstallEmptyDatabase)] = "true"; - var hostBuilder = CreateHostBuilder() - .UseUmbraco(); // This ensures CoreRuntime.StartAsync will be called (however it's a mock if boot = false) + var hostBuilder = CreateHostBuilder(); IHost host = hostBuilder.Build(); - Services = host.Services; + BeforeHostStart(host); host.Start(); var app = new ApplicationBuilder(host.Services); - Configure(app); } + protected void BeforeHostStart(IHost host) + { + Services = host.Services; + UseTestDatabase(Services); + } + #region Generic Host Builder and Runtime private ILoggerFactory CreateLoggerFactory() @@ -149,12 +153,12 @@ namespace Umbraco.Tests.Integration.Testing public virtual IHostBuilder CreateHostBuilder() { var hostBuilder = Host.CreateDefaultBuilder() + .UseUmbraco() // 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()); }) - .ConfigureAppConfiguration((context, configBuilder) => { context.HostingEnvironment = TestHelper.GetWebHostEnvironment(); @@ -233,8 +237,6 @@ namespace Umbraco.Tests.Integration.Testing public virtual void Configure(IApplicationBuilder app) { - UseTestDatabase(app.ApplicationServices); - if (TestOptions.Boot) { Services.GetRequiredService().EnsureBackOfficeSecurity(); From 3395f4cc35e05fc098f9424d8125fd22f4a76877 Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Thu, 17 Dec 2020 12:55:59 +0000 Subject: [PATCH 08/11] Fixes for ContentTypeServiceVariantsTests PublishedSnapshotService hack These pass if running the full suite, but fail when ran alone otherwise --- .../ContentTypeServiceVariantsTests.cs | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs index d11210c093..e9ec0cbae0 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -10,10 +10,12 @@ using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Services; using Umbraco.Core.Sync; +using Umbraco.Net; using Umbraco.Tests.Common.Builders; using Umbraco.Tests.Integration.Testing; using Umbraco.Tests.Testing; using Umbraco.Web.PublishedCache; +using Umbraco.Web.PublishedCache.NuCache; namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services { @@ -510,7 +512,9 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services { // one simple content type, variant, with both variant and invariant properties // can change it to invariant and back - GetRequiredService(); //hack to ensure events are initialized + + //hack to ensure events are initialized + (GetRequiredService() as PublishedSnapshotService)?.OnApplicationInit(null, null); CreateFrenchAndEnglishLangs(); var contentType = CreateContentType(ContentVariation.Culture); @@ -599,7 +603,9 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services // one simple content type, invariant // can change it to variant and back // can then switch one property to variant - GetRequiredService(); //hack to ensure events are initialized + + //hack to ensure events are initialized + (GetRequiredService() as PublishedSnapshotService)?.OnApplicationInit(null, null); var globalSettings = new GlobalSettings(); @@ -690,7 +696,9 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services { // one simple content type, variant, with both variant and invariant properties // can change an invariant property to variant and back - GetRequiredService(); //hack to ensure events are initialized + + //hack to ensure events are initialized + (GetRequiredService() as PublishedSnapshotService)?.OnApplicationInit(null, null); CreateFrenchAndEnglishLangs(); var contentType = CreateContentType(ContentVariation.Culture); @@ -967,8 +975,8 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services // can change the composing content type to invariant and back // can change the composed content type to invariant and back - - GetRequiredService(); //hack to ensure events are initialized + //hack to ensure events are initialized + (GetRequiredService() as PublishedSnapshotService)?.OnApplicationInit(null, null); CreateFrenchAndEnglishLangs(); var composing = CreateContentType(ContentVariation.Culture, "composing"); @@ -1064,7 +1072,8 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services // can change the composing content type to invariant and back // can change the variant composed content type to invariant and back - GetRequiredService(); //hack to ensure events are initialized + //hack to ensure events are initialized + (GetRequiredService() as PublishedSnapshotService)?.OnApplicationInit(null, null); CreateFrenchAndEnglishLangs(); var composing = CreateContentType(ContentVariation.Culture, "composing"); From fa061d36ba4464d0b7d3a8fec916ee6102dc0f8a Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Thu, 17 Dec 2020 17:06:51 +0000 Subject: [PATCH 09/11] What's up with ThreadSafetyServiceTest? Really interesting that it has this at top if (Environment.GetEnvironmentVariable("UMBRACO_TMP") != null) Assert.Ignore("Do not run on VSTS."); But this test passes on other peoples builds :( --- .../Services/ThreadSafetyServiceTest.cs | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ThreadSafetyServiceTest.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ThreadSafetyServiceTest.cs index 1d3b799679..9600d62051 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ThreadSafetyServiceTest.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ThreadSafetyServiceTest.cs @@ -1,8 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; +using Microsoft.Extensions.Logging; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Models; @@ -31,7 +32,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services [TestFixture] [Apartment(ApartmentState.STA)] - [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Console)] public class ThreadSafetyServiceTest : UmbracoIntegrationTest { private IContentService ContentService => GetRequiredService(); @@ -98,13 +99,15 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services if (Environment.GetEnvironmentVariable("UMBRACO_TMP") != null) Assert.Ignore("Do not run on VSTS."); + var log = GetRequiredService>(); + // the ServiceContext in that each repository in a service (i.e. ContentService) is a singleton var contentService = (ContentService)ContentService; var threads = new List(); var exceptions = new List(); - Debug.WriteLine("Starting..."); + log.LogInformation("Starting..."); var done = TraceLocks(); @@ -114,12 +117,12 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services { try { - Debug.WriteLine("[{0}] Running...", Thread.CurrentThread.ManagedThreadId); + log.LogInformation("[{0}] Running...", Thread.CurrentThread.ManagedThreadId); var name1 = "test-" + Guid.NewGuid(); var content1 = contentService.Create(name1, -1, "umbTextpage"); - Debug.WriteLine("[{0}] Saving content #1.", Thread.CurrentThread.ManagedThreadId); + log.LogInformation("[{0}] Saving content #1.", Thread.CurrentThread.ManagedThreadId); Save(contentService, content1); Thread.Sleep(100); //quick pause for maximum overlap! @@ -127,7 +130,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services var name2 = "test-" + Guid.NewGuid(); var content2 = contentService.Create(name2, -1, "umbTextpage"); - Debug.WriteLine("[{0}] Saving content #2.", Thread.CurrentThread.ManagedThreadId); + log.LogInformation("[{0}] Saving content #2.", Thread.CurrentThread.ManagedThreadId); Save(contentService, content2); } catch (Exception e) @@ -139,16 +142,16 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services } // start all threads - Debug.WriteLine("Starting threads"); + log.LogInformation("Starting threads"); threads.ForEach(x => x.Start()); // wait for all to complete - Debug.WriteLine("Joining threads"); + log.LogInformation("Joining threads"); threads.ForEach(x => x.Join()); done.Set(); - Debug.WriteLine("Checking exceptions"); + log.LogInformation("Checking exceptions"); if (exceptions.Count == 0) { //now look up all items, there should be 40! @@ -166,13 +169,16 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services { if (Environment.GetEnvironmentVariable("UMBRACO_TMP") != null) Assert.Ignore("Do not run on VSTS."); + + var log = GetRequiredService>(); + // mimick the ServiceContext in that each repository in a service (i.e. ContentService) is a singleton var mediaService = (MediaService)MediaService; var threads = new List(); var exceptions = new List(); - Debug.WriteLine("Starting..."); + log.LogInformation("Starting..."); var done = TraceLocks(); @@ -182,18 +188,18 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services { try { - Debug.WriteLine("[{0}] Running...", Thread.CurrentThread.ManagedThreadId); + log.LogInformation("[{0}] Running...", Thread.CurrentThread.ManagedThreadId); var name1 = "test-" + Guid.NewGuid(); var media1 = mediaService.CreateMedia(name1, -1, Constants.Conventions.MediaTypes.Folder); - Debug.WriteLine("[{0}] Saving media #1.", Thread.CurrentThread.ManagedThreadId); + log.LogInformation("[{0}] Saving media #1.", Thread.CurrentThread.ManagedThreadId); Save(mediaService, media1); Thread.Sleep(100); //quick pause for maximum overlap! var name2 = "test-" + Guid.NewGuid(); var media2 = mediaService.CreateMedia(name2, -1, Constants.Conventions.MediaTypes.Folder); - Debug.WriteLine("[{0}] Saving media #2.", Thread.CurrentThread.ManagedThreadId); + log.LogInformation("[{0}] Saving media #2.", Thread.CurrentThread.ManagedThreadId); Save(mediaService, media2); } catch (Exception e) From 6114fffb4a97d6a71bc5b57cfb877d18458ac679 Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Fri, 18 Dec 2020 09:53:01 +0000 Subject: [PATCH 10/11] Remove HostBuilder UseUmbraco extension, it's not required. --- .../Composing/HostBuilderExtensions.cs | 25 ------------------- src/Umbraco.Tests.Integration/RuntimeTests.cs | 1 - .../Testing/UmbracoIntegrationTest.cs | 1 - .../Builder/UmbracoBuilderExtensions.cs | 2 ++ src/Umbraco.Web.UI.NetCore/Program.cs | 1 - 5 files changed, 2 insertions(+), 28 deletions(-) delete mode 100644 src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs diff --git a/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs b/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs deleted file mode 100644 index 8e1176747c..0000000000 --- a/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace Umbraco.Core.Composing -{ - /// - /// Extends the to add CoreRuntime as a HostedService - /// - public static class HostBuilderExtensions - { - /// - /// Adds CoreRuntime as HostedService - /// - /// - /// Should be called before ConfigureWebDefaults. - /// - public static IHostBuilder UseUmbraco(this IHostBuilder builder) - { - _ = builder.ConfigureServices((context, services) => - services.AddSingleton(factory => factory.GetRequiredService())); - - return builder; - } - } -} diff --git a/src/Umbraco.Tests.Integration/RuntimeTests.cs b/src/Umbraco.Tests.Integration/RuntimeTests.cs index cd6909a8c5..f74a95af4c 100644 --- a/src/Umbraco.Tests.Integration/RuntimeTests.cs +++ b/src/Umbraco.Tests.Integration/RuntimeTests.cs @@ -43,7 +43,6 @@ namespace Umbraco.Tests.Integration var testHelper = new TestHelper(); var hostBuilder = new HostBuilder() - .UseUmbraco() .ConfigureServices((hostContext, services) => { var webHostEnvironment = testHelper.GetWebHostEnvironment(); diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index e62b3f5c7f..d3abd76d7b 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -153,7 +153,6 @@ namespace Umbraco.Tests.Integration.Testing public virtual IHostBuilder CreateHostBuilder() { var hostBuilder = Host.CreateDefaultBuilder() - .UseUmbraco() // 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, diff --git a/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs index b8dccfbe33..445025a80c 100644 --- a/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs @@ -146,7 +146,9 @@ namespace Umbraco.Core.DependencyInjection builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddHostedService(factory => factory.GetRequiredService()); builder.AddCoreInitialServices(); builder.AddComposers(); diff --git a/src/Umbraco.Web.UI.NetCore/Program.cs b/src/Umbraco.Web.UI.NetCore/Program.cs index 95322cb1b0..4849ee226a 100644 --- a/src/Umbraco.Web.UI.NetCore/Program.cs +++ b/src/Umbraco.Web.UI.NetCore/Program.cs @@ -20,7 +20,6 @@ namespace Umbraco.Web.UI.NetCore { x.ClearProviders(); }) - .UseUmbraco() .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); } } From e4e26159aea883d4e2b0012965ee2178ff1b1082 Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Fri, 18 Dec 2020 14:10:11 +0000 Subject: [PATCH 11/11] Prevent multiple first time schema create --- .../Testing/BaseTestDatabase.cs | 70 +++++++++++-------- .../Testing/LocalDbTestDatabase.cs | 2 - .../Testing/SqlDeveloperTestDatabase.cs | 2 - 3 files changed, 39 insertions(+), 35 deletions(-) diff --git a/src/Umbraco.Tests.Integration/Testing/BaseTestDatabase.cs b/src/Umbraco.Tests.Integration/Testing/BaseTestDatabase.cs index 2a9733fd9d..52a0778a59 100644 --- a/src/Umbraco.Tests.Integration/Testing/BaseTestDatabase.cs +++ b/src/Umbraco.Tests.Integration/Testing/BaseTestDatabase.cs @@ -19,7 +19,9 @@ namespace Umbraco.Tests.Integration.Testing protected IUmbracoDatabaseFactory _databaseFactory; protected IList _testDatabases; - protected UmbracoDatabase.CommandInfo[] _cachedDatabaseInitCommands; + protected const int _threadCount = 2; + + protected UmbracoDatabase.CommandInfo[] _cachedDatabaseInitCommands = new UmbracoDatabase.CommandInfo[0]; protected BlockingCollection _prepareQueue; protected BlockingCollection _readySchemaQueue; @@ -92,46 +94,52 @@ namespace Umbraco.Tests.Integration.Testing }); } - protected void RebuildSchema(IDbCommand command, TestDbMeta meta) + private void RebuildSchema(IDbCommand command, TestDbMeta meta) { - if (_cachedDatabaseInitCommands != null) + lock (_cachedDatabaseInitCommands) { - foreach (var dbCommand in _cachedDatabaseInitCommands) + if (!_cachedDatabaseInitCommands.Any()) { - - if (dbCommand.Text.StartsWith("SELECT ")) - { - continue; - } - - command.CommandText = dbCommand.Text; - command.Parameters.Clear(); - - foreach (var parameterInfo in dbCommand.Parameters) - { - AddParameter(command, parameterInfo); - } - - command.ExecuteNonQuery(); + RebuildSchemaFirstTime(command, meta); + return; } } - else + + foreach (var dbCommand in _cachedDatabaseInitCommands) { - _databaseFactory.Configure(meta.ConnectionString, Core.Constants.DatabaseProviders.SqlServer); - - using (var database = (UmbracoDatabase)_databaseFactory.CreateDatabase()) + if (dbCommand.Text.StartsWith("SELECT ")) { - database.LogCommands = true; + continue; + } - using (var transaction = database.GetTransaction()) - { - var schemaCreator = new DatabaseSchemaCreator(database, _loggerFactory.CreateLogger(), _loggerFactory, new UmbracoVersion()); - schemaCreator.InitializeDatabaseSchema(); + command.CommandText = dbCommand.Text; + command.Parameters.Clear(); - transaction.Complete(); + foreach (var parameterInfo in dbCommand.Parameters) + { + AddParameter(command, parameterInfo); + } - _cachedDatabaseInitCommands = database.Commands.ToArray(); - } + command.ExecuteNonQuery(); + } + } + + private void RebuildSchemaFirstTime(IDbCommand command, TestDbMeta meta) + { + _databaseFactory.Configure(meta.ConnectionString, Core.Constants.DatabaseProviders.SqlServer); + + using (var database = (UmbracoDatabase)_databaseFactory.CreateDatabase()) + { + database.LogCommands = true; + + using (var transaction = database.GetTransaction()) + { + var schemaCreator = new DatabaseSchemaCreator(database, _loggerFactory.CreateLogger(), _loggerFactory, new UmbracoVersion()); + schemaCreator.InitializeDatabaseSchema(); + + transaction.Complete(); + + _cachedDatabaseInitCommands = database.Commands.ToArray(); } } } diff --git a/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs b/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs index add3df9fe8..59a9b00215 100644 --- a/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs +++ b/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs @@ -19,8 +19,6 @@ namespace Umbraco.Tests.Integration.Testing private static LocalDb.Instance _localDbInstance; private static string _filesPath; - private const int _threadCount = 2; - public static LocalDbTestDatabase Instance { get; private set; } //It's internal because `Umbraco.Core.Persistence.LocalDb` is internal diff --git a/src/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs b/src/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs index 4a7f602ac6..2e1f75cd45 100644 --- a/src/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs +++ b/src/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs @@ -17,8 +17,6 @@ namespace Umbraco.Tests.Integration.Testing private readonly string _masterConnectionString; public const string DatabaseName = "UmbracoTests"; - private const int _threadCount = 2; - public static SqlDeveloperTestDatabase Instance { get; private set; } public SqlDeveloperTestDatabase(ILoggerFactory loggerFactory, IUmbracoDatabaseFactory databaseFactory, string masterConnectionString)