diff --git a/.gitignore b/.gitignore index 12ad3299ad..8ff5a8ef25 100644 --- a/.gitignore +++ b/.gitignore @@ -164,3 +164,5 @@ build/temp/ # eof /src/Umbraco.Web.UI.Client/TESTS-*.xml /src/ApiDocs/api/* +/src/Umbraco.Web.UI.NetCore/wwwroot/Media/* +/src/Umbraco.Web.UI.NetCore/wwwroot/is-cache/* diff --git a/src/Umbraco.Configuration/AspNetCoreConfigsFactory.cs b/src/Umbraco.Configuration/AspNetCoreConfigsFactory.cs index 1e9f7976d5..0cacab9e1d 100644 --- a/src/Umbraco.Configuration/AspNetCoreConfigsFactory.cs +++ b/src/Umbraco.Configuration/AspNetCoreConfigsFactory.cs @@ -14,7 +14,7 @@ namespace Umbraco.Configuration public AspNetCoreConfigsFactory(IConfiguration configuration) { - _configuration = configuration; + _configuration = configuration ?? throw new System.ArgumentNullException(nameof(configuration)); } public Configs Create() diff --git a/src/Umbraco.Core/Composing/FindAssembliesWithReferencesTo.cs b/src/Umbraco.Core/Composing/FindAssembliesWithReferencesTo.cs index 91225ff973..9378941166 100644 --- a/src/Umbraco.Core/Composing/FindAssembliesWithReferencesTo.cs +++ b/src/Umbraco.Core/Composing/FindAssembliesWithReferencesTo.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; @@ -42,7 +43,14 @@ namespace Umbraco.Core.Composing { foreach(var target in _targetAssemblies) { - referenceItems.Add(Assembly.Load(target)); + try + { + referenceItems.Add(Assembly.Load(target)); + } + catch (FileNotFoundException) + { + // occurs if we cannot load this ... for example in a test project where we aren't currently referencing Umbraco.Web, etc... + } } } diff --git a/src/Umbraco.Core/IO/IOHelper.cs b/src/Umbraco.Core/IO/IOHelper.cs index 3abd413fd4..9eddea6477 100644 --- a/src/Umbraco.Core/IO/IOHelper.cs +++ b/src/Umbraco.Core/IO/IOHelper.cs @@ -17,7 +17,7 @@ namespace Umbraco.Core.IO public IOHelper(IHostingEnvironment hostingEnvironment, IGlobalSettings globalSettings) { - _hostingEnvironment = hostingEnvironment; + _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); _globalSettings = globalSettings; } diff --git a/src/Umbraco.Tests/TestHelpers/ConsoleLogger.cs b/src/Umbraco.Core/Logging/ConsoleLogger.cs similarity index 98% rename from src/Umbraco.Tests/TestHelpers/ConsoleLogger.cs rename to src/Umbraco.Core/Logging/ConsoleLogger.cs index 53d6078e4b..275b8d988b 100644 --- a/src/Umbraco.Tests/TestHelpers/ConsoleLogger.cs +++ b/src/Umbraco.Core/Logging/ConsoleLogger.cs @@ -1,7 +1,6 @@ using System; -using Umbraco.Core.Logging; -namespace Umbraco.Tests.TestHelpers +namespace Umbraco.Core.Logging { public class ConsoleLogger : ILogger { diff --git a/src/Umbraco.Core/Logging/ILogger.cs b/src/Umbraco.Core/Logging/ILogger.cs index 4f49d0b3b4..fe7d798ebf 100644 --- a/src/Umbraco.Core/Logging/ILogger.cs +++ b/src/Umbraco.Core/Logging/ILogger.cs @@ -2,6 +2,7 @@ namespace Umbraco.Core.Logging { + /// /// Defines the logging service. /// diff --git a/src/Umbraco.Core/Routing/UriUtility.cs b/src/Umbraco.Core/Routing/UriUtility.cs index 9cb6bf50f6..0c68580204 100644 --- a/src/Umbraco.Core/Routing/UriUtility.cs +++ b/src/Umbraco.Core/Routing/UriUtility.cs @@ -15,6 +15,7 @@ namespace Umbraco.Web public UriUtility(IHostingEnvironment hostingEnvironment) { + if (hostingEnvironment is null) throw new ArgumentNullException(nameof(hostingEnvironment)); ResetAppDomainAppVirtualPath(hostingEnvironment); } diff --git a/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs b/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs new file mode 100644 index 0000000000..d5355c136f --- /dev/null +++ b/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.Hosting; + +namespace Umbraco.Core.Composing +{ + /// + /// Extends the to enable Umbraco to be used as the service container. + /// + public static class HostBuilderExtensions + { + /// + /// Assigns a custom service provider factory to use Umbraco's container + /// + /// + /// + public static IHostBuilder UseUmbraco(this IHostBuilder builder) + { + return builder.UseServiceProviderFactory(new UmbracoServiceProviderFactory()); + } + } +} diff --git a/src/Umbraco.Infrastructure/Composing/LightInject/LightInjectContainer.cs b/src/Umbraco.Infrastructure/Composing/LightInject/LightInjectContainer.cs index 5200dced90..a6db0b7b2b 100644 --- a/src/Umbraco.Infrastructure/Composing/LightInject/LightInjectContainer.cs +++ b/src/Umbraco.Infrastructure/Composing/LightInject/LightInjectContainer.cs @@ -16,11 +16,13 @@ namespace Umbraco.Core.Composing.LightInject /// /// Initializes a new instance of the with a LightInject container. /// - protected LightInjectContainer(ServiceContainer container) + public LightInjectContainer(ServiceContainer container) { - Container = container; + Container = ConfigureContainer(container); } + //TODO: The Create methods can die when net framework is gone + /// /// Creates a new instance of the class. /// @@ -33,7 +35,12 @@ namespace Umbraco.Core.Composing.LightInject protected static ServiceContainer CreateServiceContainer() { var container = new ServiceContainer(new ContainerOptions { EnablePropertyInjection = false }); + ConfigureContainer(container); + return container; + } + private static ServiceContainer ConfigureContainer(ServiceContainer container) + { // note: the block below is disabled, as it is too LightInject-specific // // supports annotated constructor injections @@ -84,7 +91,7 @@ namespace Umbraco.Core.Composing.LightInject /// /// Gets the LightInject container. /// - protected ServiceContainer Container { get; } + public ServiceContainer Container { get; } /// /// diff --git a/src/Umbraco.Infrastructure/Composing/RegisterFactory.cs b/src/Umbraco.Infrastructure/Composing/RegisterFactory.cs index 8f842e14fe..835bd0b9a8 100644 --- a/src/Umbraco.Infrastructure/Composing/RegisterFactory.cs +++ b/src/Umbraco.Infrastructure/Composing/RegisterFactory.cs @@ -1,14 +1,22 @@ -using System; +using LightInject; +using LightInject.Microsoft.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; using System.Reflection; +using Umbraco.Core.Composing.LightInject; using Umbraco.Core.Configuration; namespace Umbraco.Core.Composing { + /// /// Creates the container. /// public static class RegisterFactory { + //TODO: This can die when net framework is gone + // cannot use typeof().AssemblyQualifiedName on the web container - we don't reference it // a normal Umbraco site should run on the web container, but an app may run on the core one private const string CoreLightInjectContainerTypeName = "Umbraco.Core.Composing.LightInject.LightInjectContainer,Umbraco.Core"; diff --git a/src/Umbraco.Infrastructure/Composing/UmbracoServiceProviderFactory.cs b/src/Umbraco.Infrastructure/Composing/UmbracoServiceProviderFactory.cs new file mode 100644 index 0000000000..6fd0bda61e --- /dev/null +++ b/src/Umbraco.Infrastructure/Composing/UmbracoServiceProviderFactory.cs @@ -0,0 +1,78 @@ +using LightInject; +using LightInject.Microsoft.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using Umbraco.Core.Composing.LightInject; + +namespace Umbraco.Core.Composing +{ + /// + /// Used to create Umbraco's container and cross-wire it up before the applicaton starts + /// + public class UmbracoServiceProviderFactory : IServiceProviderFactory + { + public UmbracoServiceProviderFactory(ServiceContainer container) + { + _container = new LightInjectContainer(container); + } + + /// + /// Creates an ASP.NET Core compatible service container + /// + /// + public static ServiceContainer CreateServiceContainer() => new ServiceContainer(ContainerOptions.Default.Clone().WithMicrosoftSettings().WithAspNetCoreSettings()); + + /// + /// Default ctor for use in Host Builder configuration + /// + public UmbracoServiceProviderFactory() + { + var container = CreateServiceContainer(); + UmbracoContainer = _container = new LightInjectContainer(container); + IsActive = true; + } + + // see here for orig lightinject version https://github.com/seesharper/LightInject.Microsoft.DependencyInjection/blob/412566e3f70625e6b96471db5e1f7cd9e3e1eb18/src/LightInject.Microsoft.DependencyInjection/LightInject.Microsoft.DependencyInjection.cs#L263 + // we don't really need all that, we're manually creating our container with the correct options and that + // is what we'll return in CreateBuilder + + IServiceCollection _services; + readonly LightInjectContainer _container; + + internal LightInjectContainer GetContainer() => _container; + + /// + /// When the empty ctor is used this returns if this factory is active + /// + public static bool IsActive { get; private set; } + + /// + /// When the empty ctor is used this returns the created IRegister + /// + public static IRegister UmbracoContainer { get; private set; } + + /// + /// Create the container with the required settings for aspnetcore3 + /// + /// + /// + public IServiceContainer CreateBuilder(IServiceCollection services) + { + _services = services; + return _container.Container; + } + + /// + /// This cross-wires the container just before the application calls "Configure" + /// + /// + /// + public IServiceProvider CreateServiceProvider(IServiceContainer containerBuilder) + { + var provider = containerBuilder.CreateServiceProvider(_services); + return provider; + } + + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs index 23ef0bfda5..5806bb90ec 100644 --- a/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs @@ -1,9 +1,9 @@ using System.Data.Common; -using StackExchange.Profiling.Internal; using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence { + public interface IDbProviderFactoryCreator { DbProviderFactory CreateFactory(); diff --git a/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs new file mode 100644 index 0000000000..9c2c6273c2 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs @@ -0,0 +1,55 @@ +using System; +using System.Data.Common; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence +{ + public class SqlServerDbProviderFactoryCreator : IDbProviderFactoryCreator + { + private readonly string _defaultProviderName; + private readonly Func _getFactory; + + public SqlServerDbProviderFactoryCreator(string defaultProviderName, Func getFactory) + { + _defaultProviderName = defaultProviderName; + _getFactory = getFactory; + } + + public DbProviderFactory CreateFactory() => CreateFactory(_defaultProviderName); + + public DbProviderFactory CreateFactory(string providerName) + { + if (string.IsNullOrEmpty(providerName)) return null; + return _getFactory(providerName); + } + + // gets the sql syntax provider that corresponds, from attribute + public ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName) + { + return providerName switch + { + Constants.DbProviderNames.SqlCe => throw new NotSupportedException("SqlCe is not supported"), + Constants.DbProviderNames.SqlServer => new SqlServerSyntaxProvider(), + _ => throw new InvalidOperationException($"Unknown provider name \"{providerName}\""), + }; + } + + public IBulkSqlInsertProvider CreateBulkSqlInsertProvider(string providerName) + { + switch (providerName) + { + case Constants.DbProviderNames.SqlCe: + throw new NotSupportedException("SqlCe is not supported"); + case Constants.DbProviderNames.SqlServer: + return new SqlServerBulkSqlInsertProvider(); + default: + return new BasicBulkSqlInsertProvider(); + } + } + + public void CreateDatabase() + { + throw new NotSupportedException("Embedded databases are not supported"); + } + } +} diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs index 94497d2e13..6c1a06ab6b 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs @@ -41,7 +41,8 @@ namespace Umbraco.Core.Runtime IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo, IDbProviderFactoryCreator dbProviderFactoryCreator, - IMainDom mainDom) + IMainDom mainDom, + ITypeFinder typeFinder) { IOHelper = ioHelper; Configs = configs; @@ -55,6 +56,7 @@ namespace Umbraco.Core.Runtime Logger = logger; MainDom = mainDom; + TypeFinder = typeFinder; _globalSettings = Configs.Global(); _connectionStrings = configs.ConnectionStrings(); @@ -95,7 +97,7 @@ namespace Umbraco.Core.Runtime /// /// Gets the /// - protected ITypeFinder TypeFinder { get; private set; } + protected ITypeFinder TypeFinder { get; } /// /// Gets the @@ -116,11 +118,6 @@ namespace Umbraco.Core.Runtime // create and register the essential services // ie the bare minimum required to boot - - TypeFinder = GetTypeFinder(); - if (TypeFinder == null) - throw new InvalidOperationException($"The object returned from {nameof(GetTypeFinder)} cannot be null"); - // the boot loader boots using a container scope, so anything that is PerScope will // be disposed after the boot loader has booted, and anything else will remain. // note that this REQUIRES that perWebRequestScope has NOT been enabled yet, else @@ -260,11 +257,6 @@ namespace Umbraco.Core.Runtime return _factory; } - private IUmbracoVersion GetUmbracoVersion(IGlobalSettings globalSettings) - { - return new UmbracoVersion(globalSettings); - } - protected virtual void ConfigureUnhandledException() { //take care of unhandled exceptions - there is nothing we can do to @@ -371,28 +363,6 @@ namespace Umbraco.Core.Runtime protected virtual IEnumerable GetComposerTypes(TypeLoader typeLoader) => typeLoader.GetTypes(); - /// - /// Gets a - /// - /// - protected virtual ITypeFinder GetTypeFinder() - // TODO: Currently we are not passing in any TypeFinderConfig (with ITypeFinderSettings) which we should do, however - // this is not critical right now and would require loading in some config before boot time so just leaving this as-is for now. - => new TypeFinder(Logger, new DefaultUmbracoAssemblyProvider( - // GetEntryAssembly was actually an exposed API by request of the aspnetcore team which works in aspnet core because a website - // in that case is essentially an exe. However in netframework there is no entry assembly, things don't really work that way since - // the process that is running the site is iisexpress, so this returns null. The best we can do is fallback to GetExecutingAssembly() - // which will just return Umbraco.Infrastructure (currently with netframework) and for our purposes that is OK. - // If you are curious... There is really no way to get the entry assembly in netframework without the hosting website having it's own - // code compiled for the global.asax which is the entry point. Because the default global.asax for umbraco websites is just a file inheriting - // from Umbraco.Web.UmbracoApplication, the global.asax file gets dynamically compiled into a DLL in the dynamic folder (we can get an instance - // of that, but this doesn't really help us) but the actually entry execution is still Umbraco.Web. So that is the 'highest' level entry point - // assembly we can get and we can only get that if we put this code into the WebRuntime since the executing assembly is the 'current' one. - // For this purpose, it doesn't matter if it's Umbraco.Web or Umbraco.Infrastructure since all assemblies are in that same path and we are - // getting rid of netframework. - Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly())); - - /// /// Gets the application caches. /// diff --git a/src/Umbraco.Infrastructure/Runtime/WebRuntime.cs b/src/Umbraco.Infrastructure/Runtime/WebRuntime.cs index 12b12616f0..2f45a3e437 100644 --- a/src/Umbraco.Infrastructure/Runtime/WebRuntime.cs +++ b/src/Umbraco.Infrastructure/Runtime/WebRuntime.cs @@ -31,9 +31,10 @@ namespace Umbraco.Web.Runtime IBackOfficeInfo backOfficeInfo, IDbProviderFactoryCreator dbProviderFactoryCreator, IMainDom mainDom, + ITypeFinder typeFinder, IRequestCache requestCache, IUmbracoBootPermissionChecker umbracoBootPermissionChecker): - base(configs, umbracoVersion, ioHelper, logger, profiler ,umbracoBootPermissionChecker, hostingEnvironment, backOfficeInfo, dbProviderFactoryCreator, mainDom) + base(configs, umbracoVersion, ioHelper, logger, profiler ,umbracoBootPermissionChecker, hostingEnvironment, backOfficeInfo, dbProviderFactoryCreator, mainDom, typeFinder) { _requestCache = requestCache; } diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 00f48479f8..3d99c4918f 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -9,8 +9,11 @@ + + + @@ -57,6 +60,9 @@ <_Parameter1>Umbraco.Tests.Benchmarks + + <_Parameter1>Umbraco.Tests.Integration + diff --git a/src/Umbraco.Tests.Common/Assertions.cs b/src/Umbraco.Tests.Common/Assertions.cs new file mode 100644 index 0000000000..0f99a6a091 --- /dev/null +++ b/src/Umbraco.Tests.Common/Assertions.cs @@ -0,0 +1,35 @@ +using LightInject; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Umbraco.Tests.Common.Composing; + +namespace Umbraco.Tests.Common +{ + public class Assertions + { + public static void AssertContainer(ServiceContainer container, bool reportOnly = false) + { + var results = container.Validate().ToList(); + foreach (var resultGroup in results.GroupBy(x => x.Severity).OrderBy(x => x.Key)) + { + Console.WriteLine($"{resultGroup.Key}: {resultGroup.Count()}"); + } + + foreach (var resultGroup in results.GroupBy(x => x.Severity).OrderBy(x => x.Key)) + { + foreach (var result in resultGroup) + { + Console.WriteLine(); + Console.Write(result.ToText()); + } + } + + if (!reportOnly) + Assert.AreEqual(0, results.Count); + } + + } +} diff --git a/src/Umbraco.Tests/Composing/LightInjectValidation.cs b/src/Umbraco.Tests.Common/Composing/LightInjectValidation.cs similarity index 92% rename from src/Umbraco.Tests/Composing/LightInjectValidation.cs rename to src/Umbraco.Tests.Common/Composing/LightInjectValidation.cs index 75062e613c..4925074b9e 100644 --- a/src/Umbraco.Tests/Composing/LightInjectValidation.cs +++ b/src/Umbraco.Tests.Common/Composing/LightInjectValidation.cs @@ -35,7 +35,7 @@ using ServiceMap = System.Collections.Generic.Dictionary 1; } } diff --git a/src/Umbraco.Tests.Common/Composing/ValidationResultExtensions.cs b/src/Umbraco.Tests.Common/Composing/ValidationResultExtensions.cs new file mode 100644 index 0000000000..cfd136b63c --- /dev/null +++ b/src/Umbraco.Tests.Common/Composing/ValidationResultExtensions.cs @@ -0,0 +1,107 @@ +using System; +using System.Text; +using Umbraco.Core; + +namespace Umbraco.Tests.Common.Composing +{ + // These are used for Light Inject container validation + public static class ValidationResultExtensions + { + public static string ToText(this ValidationResult result) + { + var text = new StringBuilder(); + + text.AppendLine($"{result.Severity}: {WordWrap(result.Message, 120)}"); + var target = result.ValidationTarget; + text.Append("\tsvce: "); + text.Append(target.ServiceName); + text.Append(target.DeclaringService.ServiceType); + if (!target.DeclaringService.ServiceName.IsNullOrWhiteSpace()) + { + text.Append(" '"); + text.Append(target.DeclaringService.ServiceName); + text.Append("'"); + } + + text.Append(" ("); + if (target.DeclaringService.Lifetime == null) + text.Append("Transient"); + else + text.Append(target.DeclaringService.Lifetime.ToString().TrimStart("LightInject.").TrimEnd("Lifetime")); + text.AppendLine(")"); + text.Append("\timpl: "); + text.Append(target.DeclaringService.ImplementingType); + text.AppendLine(); + text.Append("\tparm: "); + text.Append(target.Parameter); + text.AppendLine(); + + return text.ToString(); + } + + private static string WordWrap(string text, int width) + { + int pos, next; + var sb = new StringBuilder(); + var nl = Environment.NewLine; + + // Lucidity check + if (width < 1) + return text; + + // Parse each line of text + for (pos = 0; pos < text.Length; pos = next) + { + // Find end of line + var eol = text.IndexOf(nl, pos, StringComparison.Ordinal); + + if (eol == -1) + next = eol = text.Length; + else + next = eol + nl.Length; + + // Copy this line of text, breaking into smaller lines as needed + if (eol > pos) + { + do + { + var len = eol - pos; + + if (len > width) + len = BreakLine(text, pos, width); + + if (pos > 0) + sb.Append("\t\t"); + sb.Append(text, pos, len); + sb.Append(nl); + + // Trim whitespace following break + pos += len; + + while (pos < eol && char.IsWhiteSpace(text[pos])) + pos++; + + } while (eol > pos); + } + else sb.Append(nl); // Empty line + } + + return sb.ToString(); + } + + private static int BreakLine(string text, int pos, int max) + { + // Find last whitespace in line + var i = max - 1; + while (i >= 0 && !char.IsWhiteSpace(text[pos + i])) + i--; + if (i < 0) + return max; // No whitespace found; break at maximum length + // Find start of whitespace + while (i >= 0 && char.IsWhiteSpace(text[pos + i])) + i--; + // Return length of text before whitespace + return i + 1; + } + } +} diff --git a/src/Umbraco.Tests.Common/TestHelperBase.cs b/src/Umbraco.Tests.Common/TestHelperBase.cs index d21aff89d9..536bebee56 100644 --- a/src/Umbraco.Tests.Common/TestHelperBase.cs +++ b/src/Umbraco.Tests.Common/TestHelperBase.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Reflection; using Moq; using Umbraco.Core; using Umbraco.Core.Cache; @@ -12,7 +13,6 @@ using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Persistence; -using Umbraco.Core.Runtime; using Umbraco.Core.Serialization; using Umbraco.Core.Strings; using Umbraco.Core.Sync; @@ -27,31 +27,26 @@ namespace Umbraco.Tests.Common /// public abstract class TestHelperBase { - public TestHelperBase() + private readonly ITypeFinder _typeFinder; + private UriUtility _uriUtility; + private IIOHelper _ioHelper; + + public TestHelperBase(Assembly entryAssembly) { - SettingsForTests = new SettingsForTests(); - IOHelper = new IOHelper(GetHostingEnvironment(), SettingsForTests.GenerateMockGlobalSettings()); - MainDom = new MainDom(Mock.Of(), GetHostingEnvironment(), new MainDomSemaphoreLock(Mock.Of(), GetHostingEnvironment())); - UriUtility = new UriUtility(GetHostingEnvironment()); + SettingsForTests = new SettingsForTests(); + MainDom = new SimpleMainDom(); + _typeFinder = new TypeFinder(Mock.Of(), new DefaultUmbracoAssemblyProvider(entryAssembly)); } - public ITypeFinder GetTypeFinder() - { - - var typeFinder = new TypeFinder(Mock.Of(), - new DefaultUmbracoAssemblyProvider(typeof(TestHelperBase).Assembly)); - return typeFinder; - } + public ITypeFinder GetTypeFinder() => _typeFinder; public TypeLoader GetMockedTypeLoader() { return new TypeLoader(IOHelper, Mock.Of(), Mock.Of(), new DirectoryInfo(IOHelper.MapPath("~/App_Data/TEMP")), Mock.Of()); } - public Configs GetConfigs() - { - return GetConfigsFactory().Create(); - } + public Configs GetConfigs() => GetConfigsFactory().Create(); + public IRuntimeState GetRuntimeState() { return new RuntimeState( @@ -67,10 +62,7 @@ namespace Umbraco.Tests.Common public abstract IBackOfficeInfo GetBackOfficeInfo(); - public IConfigsFactory GetConfigsFactory() - { - return new ConfigsFactory(); - } + public IConfigsFactory GetConfigsFactory() => new ConfigsFactory(); /// /// Gets the current assembly directory. @@ -95,10 +87,27 @@ namespace Umbraco.Tests.Common public abstract IMarchal Marchal { get; } public ICoreDebugSettings CoreDebugSettings { get; } = new CoreDebugSettings(); + public IIOHelper IOHelper + { + get + { + if (_ioHelper == null) + _ioHelper = new IOHelper(GetHostingEnvironment(), SettingsForTests.GenerateMockGlobalSettings()); + return _ioHelper; + } + } - public IIOHelper IOHelper { get; } public IMainDom MainDom { get; } - public UriUtility UriUtility { get; } + public UriUtility UriUtility + { + get + { + if (_uriUtility == null) + _uriUtility = new UriUtility(GetHostingEnvironment()); + return _uriUtility; + } + } + public SettingsForTests SettingsForTests { get; } public IWebRoutingSettings WebRoutingSettings => SettingsForTests.GenerateMockWebRoutingSettings(); @@ -115,10 +124,7 @@ namespace Umbraco.Tests.Common return relativePath.Replace("~/", CurrentAssemblyDirectory + "/"); } - public IUmbracoVersion GetUmbracoVersion() - { - return new UmbracoVersion(GetConfigs().Global()); - } + public IUmbracoVersion GetUmbracoVersion() => new UmbracoVersion(GetConfigs().Global()); public IRegister GetRegister() { diff --git a/src/Umbraco.Tests.Integration/ContainerTests.cs b/src/Umbraco.Tests.Integration/ContainerTests.cs new file mode 100644 index 0000000000..d190d1165d --- /dev/null +++ b/src/Umbraco.Tests.Integration/ContainerTests.cs @@ -0,0 +1,77 @@ +using LightInject; +using LightInject.Microsoft.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Moq; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Composing; +using Umbraco.Core.Composing.LightInject; +using Umbraco.Core.Configuration; +using Umbraco.Core.Persistence; +using Umbraco.Tests.Common; +using Umbraco.Tests.Integration.Implementations; + +namespace Umbraco.Tests.Integration +{ + + [TestFixture] + public class ContainerTests + { + [Test] + public void CrossWire() + { + // MSDI + var services = new ServiceCollection(); + services.AddSingleton(); + var msdiServiceProvider = services.BuildServiceProvider(); + + // LightInject / Umbraco + var container = UmbracoServiceProviderFactory.CreateServiceContainer(); + var serviceProviderFactory = new UmbracoServiceProviderFactory(container); + var umbracoContainer = serviceProviderFactory.GetContainer(); + serviceProviderFactory.CreateBuilder(services); // called during Host Builder, needed to capture services + + // Dependencies needed for creating composition/register essentials + var testHelper = new TestHelper(); + var runtimeState = Mock.Of(); + var umbracoDatabaseFactory = Mock.Of(); + var dbProviderFactoryCreator = Mock.Of(); + var typeLoader = testHelper.GetMockedTypeLoader(); + + // Register in the container + var composition = new Composition(umbracoContainer, typeLoader, + testHelper.Logger, runtimeState, testHelper.GetConfigs(), testHelper.IOHelper, testHelper.AppCaches); + composition.RegisterEssentials(testHelper.Logger, testHelper.Profiler, testHelper.Logger, testHelper.MainDom, + testHelper.AppCaches, umbracoDatabaseFactory, typeLoader, runtimeState, testHelper.GetTypeFinder(), + testHelper.IOHelper, testHelper.GetUmbracoVersion(), dbProviderFactoryCreator); + + // Cross wire - this would be called by the Host Builder at the very end of ConfigureServices + var lightInjectServiceProvider = serviceProviderFactory.CreateServiceProvider(umbracoContainer.Container); + + // From MSDI + var foo1 = msdiServiceProvider.GetService(); + var foo2 = lightInjectServiceProvider.GetService(); + var foo3 = umbracoContainer.GetInstance(); + + Assert.IsNotNull(foo1); + Assert.IsNotNull(foo2); + Assert.IsNotNull(foo3); + + // These are not the same because cross wiring means copying the container, not falling back to a container + Assert.AreNotSame(foo1, foo2); + // These are the same because the umbraco container wraps the light inject container + Assert.AreSame(foo2, foo3); + + Assertions.AssertContainer(umbracoContainer.Container); + } + + private class Foo + { + public Foo() + { + } + } + } +} diff --git a/src/Umbraco.Tests.Integration/Implementations/TestDbProviderFactoryCreator.cs b/src/Umbraco.Tests.Integration/Implementations/TestDbProviderFactoryCreator.cs new file mode 100644 index 0000000000..9081c4ccb4 --- /dev/null +++ b/src/Umbraco.Tests.Integration/Implementations/TestDbProviderFactoryCreator.cs @@ -0,0 +1,34 @@ +using System.Data.Common; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Tests.Integration.Implementations +{ + public class TestDbProviderFactoryCreator : IDbProviderFactoryCreator + { + public IBulkSqlInsertProvider CreateBulkSqlInsertProvider(string providerName) + { + throw new System.NotImplementedException(); + } + + public void CreateDatabase() + { + throw new System.NotImplementedException(); + } + + public DbProviderFactory CreateFactory() + { + throw new System.NotImplementedException(); + } + + public DbProviderFactory CreateFactory(string providerName) + { + throw new System.NotImplementedException(); + } + + public ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName) + { + throw new System.NotImplementedException(); + } + } +} diff --git a/src/Umbraco.Tests.Integration/Implementations/TestHelper.cs b/src/Umbraco.Tests.Integration/Implementations/TestHelper.cs new file mode 100644 index 0000000000..5254892b23 --- /dev/null +++ b/src/Umbraco.Tests.Integration/Implementations/TestHelper.cs @@ -0,0 +1,80 @@ + +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Hosting; +using Moq; +using System.Net; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Diagnostics; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; +using Umbraco.Core.Runtime; +using Umbraco.Net; +using Umbraco.Tests.Common; +using Umbraco.Web.BackOffice; +using Umbraco.Web.BackOffice.AspNetCore; +using IHostingEnvironment = Umbraco.Core.Hosting.IHostingEnvironment; + +namespace Umbraco.Tests.Integration.Implementations +{ + + public class TestHelper : TestHelperBase + { + private IBackOfficeInfo _backOfficeInfo; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IIpResolver _ipResolver; + private readonly IWebHostEnvironment _hostEnvironment; + private readonly IHttpContextAccessor _httpContextAccessor; + + public TestHelper() : base(typeof(TestHelper).Assembly) + { + var httpContext = new DefaultHttpContext(); + httpContext.Connection.RemoteIpAddress = IPAddress.Parse("127.0.0.1"); + _httpContextAccessor = Mock.Of(x => x.HttpContext == httpContext); + _ipResolver = new AspNetIpResolver(_httpContextAccessor); + + _hostEnvironment = Mock.Of(x => + x.ApplicationName == "UmbracoIntegrationTests" + && x.ContentRootPath == CurrentAssemblyDirectory + && x.WebRootPath == CurrentAssemblyDirectory); // same folder for now? + + _hostingEnvironment = new AspNetCoreHostingEnvironment( + SettingsForTests.GetDefaultHostingSettings(), + _hostEnvironment, + _httpContextAccessor, + Mock.Of()); + + Logger = new ProfilingLogger(new ConsoleLogger(new MessageTemplates()), Profiler); + } + + public IUmbracoBootPermissionChecker UmbracoBootPermissionChecker { get; } = new TestUmbracoBootPermissionChecker(); + + public AppCaches AppCaches { get; } = new AppCaches(NoAppCache.Instance, NoAppCache.Instance, new IsolatedCaches(type => NoAppCache.Instance)); + + public IProfilingLogger Logger { get; private set; } + + public IProfiler Profiler { get; } = new VoidProfiler(); + + public IHttpContextAccessor GetHttpContextAccessor() => _httpContextAccessor; + + public IWebHostEnvironment GetWebHostEnvironment() => _hostEnvironment; + + public override IDbProviderFactoryCreator DbProviderFactoryCreator => new TestDbProviderFactoryCreator(); + + public override IBulkSqlInsertProvider BulkSqlInsertProvider => new SqlServerBulkSqlInsertProvider(); + + public override IMarchal Marchal { get; } = new AspNetCoreMarchal(); + + public override IBackOfficeInfo GetBackOfficeInfo() + { + if (_backOfficeInfo == null) + _backOfficeInfo = new AspNetCoreBackOfficeInfo(SettingsForTests.GetDefaultGlobalSettings(GetUmbracoVersion())); + return _backOfficeInfo; + } + + public override IHostingEnvironment GetHostingEnvironment() => _hostingEnvironment; + + public override IIpResolver GetIpResolver() => _ipResolver; + } +} diff --git a/src/Umbraco.Tests.Integration/Implementations/TestUmbracoBootPermissionChecker.cs b/src/Umbraco.Tests.Integration/Implementations/TestUmbracoBootPermissionChecker.cs new file mode 100644 index 0000000000..b4f876fc66 --- /dev/null +++ b/src/Umbraco.Tests.Integration/Implementations/TestUmbracoBootPermissionChecker.cs @@ -0,0 +1,11 @@ +using Umbraco.Core.Runtime; + +namespace Umbraco.Tests.Integration.Implementations +{ + public class TestUmbracoBootPermissionChecker : IUmbracoBootPermissionChecker + { + public void ThrowIfNotPermissions() + { + } + } +} diff --git a/src/Umbraco.Tests.Integration/RuntimeTests.cs b/src/Umbraco.Tests.Integration/RuntimeTests.cs new file mode 100644 index 0000000000..24786a1591 --- /dev/null +++ b/src/Umbraco.Tests.Integration/RuntimeTests.cs @@ -0,0 +1,128 @@ +using LightInject; +using LightInject.Microsoft.DependencyInjection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Moq; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.Logging; +using Umbraco.Core.Runtime; +using Umbraco.Tests.Common; +using Umbraco.Tests.Integration.Implementations; +using Umbraco.Web.BackOffice.AspNetCore; + +namespace Umbraco.Tests.Integration +{ + [TestFixture] + public class RuntimeTests + { + [Test] + public void BootCoreRuntime() + { + // LightInject / Umbraco + var container = UmbracoServiceProviderFactory.CreateServiceContainer(); + var serviceProviderFactory = new UmbracoServiceProviderFactory(container); + var umbracoContainer = serviceProviderFactory.GetContainer(); + + // Create the core runtime + var testHelper = new TestHelper(); + var coreRuntime = new CoreRuntime(testHelper.GetConfigs(), testHelper.GetUmbracoVersion(), + testHelper.IOHelper, testHelper.Logger, testHelper.Profiler, testHelper.UmbracoBootPermissionChecker, + testHelper.GetHostingEnvironment(), testHelper.GetBackOfficeInfo(), testHelper.DbProviderFactoryCreator, + testHelper.MainDom, testHelper.GetTypeFinder()); + + // boot it! + var factory = coreRuntime.Boot(umbracoContainer); + + Assert.IsTrue(coreRuntime.MainDom.IsMainDom); + Assert.IsNull(coreRuntime.State.BootFailedException); + Assert.AreEqual(RuntimeLevel.Install, coreRuntime.State.Level); + Assert.IsTrue(MyComposer.IsComposed); + Assert.IsTrue(MyComponent.IsInit); + Assert.IsFalse(MyComponent.IsTerminated); + + Assertions.AssertContainer(umbracoContainer.Container, reportOnly: true); // TODO Change that to false eventually when we clean up the container + + coreRuntime.Terminate(); + + Assert.IsTrue(MyComponent.IsTerminated); + } + + [Test] + public void AddUmbracoCore() + { + var testHelper = new TestHelper(); + + // MSDI + var services = new ServiceCollection(); + // These services are required + services.AddSingleton(x => testHelper.GetHttpContextAccessor()); + services.AddSingleton(x => testHelper.GetWebHostEnvironment()); + services.AddSingleton(x => Mock.Of()); + + // LightInject / Umbraco + var container = UmbracoServiceProviderFactory.CreateServiceContainer(); + var serviceProviderFactory = new UmbracoServiceProviderFactory(container); + var umbracoContainer = serviceProviderFactory.GetContainer(); + + // Some IConfiguration must exist in the container first + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(); + services.AddSingleton(x => configurationBuilder.Build()); + + // Add it! + services.AddUmbracoConfiguration(); + services.AddUmbracoCore(umbracoContainer, GetType().Assembly); + + // assert results + var runtimeState = umbracoContainer.GetInstance(); + var mainDom = umbracoContainer.GetInstance(); + + Assert.IsTrue(mainDom.IsMainDom); + Assert.IsNull(runtimeState.BootFailedException); + Assert.AreEqual(RuntimeLevel.Install, runtimeState.Level); + Assert.IsTrue(MyComposer.IsComposed); + } + + [RuntimeLevel(MinLevel = RuntimeLevel.Install)] + public class MyComposer : IUserComposer + { + public void Compose(Composition composition) + { + composition.Components().Append(); + IsComposed = true; + } + + public static bool IsComposed { get; private set; } + } + + public class MyComponent : IComponent + { + public static bool IsInit { get; private set; } + public static bool IsTerminated { get; private set; } + + private readonly ILogger _logger; + + public MyComponent(ILogger logger) + { + _logger = logger; + } + + public void Initialize() + { + IsInit = true; + } + + public void Terminate() + { + IsTerminated = true; + } + } + } + + +} diff --git a/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj new file mode 100644 index 0000000000..55b3e8cdca --- /dev/null +++ b/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -0,0 +1,27 @@ + + + + Exe + netcoreapp3.1 + false + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs index b6b671fb3b..bc58282795 100644 --- a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs +++ b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs @@ -50,7 +50,7 @@ namespace Umbraco.Tests.Routing public class TestRuntime : WebRuntime { public TestRuntime(Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, ILogger logger, IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo) - : base(configs, umbracoVersion, ioHelper, Mock.Of(), Mock.Of(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, TestHelper.GetRequestCache(), new AspNetUmbracoBootPermissionChecker()) + : base(configs, umbracoVersion, ioHelper, Mock.Of(), Mock.Of(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, TestHelper.GetTypeFinder(), TestHelper.GetRequestCache(), new AspNetUmbracoBootPermissionChecker()) { } diff --git a/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs b/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs index 3c0097f27b..92ab825ee2 100644 --- a/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs +++ b/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs @@ -121,15 +121,11 @@ namespace Umbraco.Tests.Runtimes public class TestRuntime : CoreRuntime { public TestRuntime(Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, ILogger logger, IProfiler profiler, IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo) - :base(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom) + :base(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, TestHelper.GetTypeFinder()) { } - // override because we cannot use Assembly.GetEntryAssembly in Nunit tests since that is always null - protected override ITypeFinder GetTypeFinder() - => new TypeFinder(Logger, new DefaultUmbracoAssemblyProvider(GetType().Assembly)); - // must override the database factory // else BootFailedException because U cannot connect to the configured db protected internal override IUmbracoDatabaseFactory GetDatabaseFactory() @@ -181,7 +177,6 @@ namespace Umbraco.Tests.Runtimes public override void Terminate() { - ((IRegisteredObject) _mainDom).Stop(false); base.Terminate(); } diff --git a/src/Umbraco.Tests/Runtimes/StandaloneTests.cs b/src/Umbraco.Tests/Runtimes/StandaloneTests.cs index 213074caa7..0cb3f3e2e1 100644 --- a/src/Umbraco.Tests/Runtimes/StandaloneTests.cs +++ b/src/Umbraco.Tests/Runtimes/StandaloneTests.cs @@ -25,7 +25,6 @@ using Umbraco.Core.Runtime; using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Sync; -using Umbraco.Tests.Composing; using Umbraco.Tests.TestHelpers; using Umbraco.Web; using Umbraco.Web.Cache; @@ -36,6 +35,7 @@ using Umbraco.Web.Runtime; using File = System.IO.File; using Current = Umbraco.Web.Composing.Current; using Umbraco.Tests.Common; +using Umbraco.Tests.Common.Composing; namespace Umbraco.Tests.Runtimes { @@ -64,8 +64,8 @@ namespace Umbraco.Tests.Runtimes var appCaches = AppCaches.Disabled; var globalSettings = TestHelper.GetConfigs().Global(); var connectionStrings = TestHelper.GetConfigs().ConnectionStrings(); + var typeFinder = TestHelper.GetTypeFinder(); var databaseFactory = new UmbracoDatabaseFactory(logger,globalSettings, connectionStrings, new Lazy(() => factory.GetInstance()), TestHelper.DbProviderFactoryCreator); - var typeFinder = new TypeFinder(logger, new DefaultUmbracoAssemblyProvider(GetType().Assembly)); var ioHelper = TestHelper.IOHelper; var hostingEnvironment = Mock.Of(); var typeLoader = new TypeLoader(ioHelper, typeFinder, appCaches.RuntimeCache, new DirectoryInfo(ioHelper.MapPath("~/App_Data/TEMP")), profilingLogger); @@ -75,6 +75,7 @@ namespace Umbraco.Tests.Runtimes var runtimeState = new RuntimeState(logger, null, new Lazy(() => mainDom), new Lazy(() => factory.GetInstance()), umbracoVersion, hostingEnvironment, backOfficeInfo); var configs = TestHelper.GetConfigs(); var variationContextAccessor = TestHelper.VariationContextAccessor; + // create the register and the composition var register = TestHelper.GetRegister(); @@ -82,7 +83,8 @@ namespace Umbraco.Tests.Runtimes composition.RegisterEssentials(logger, profiler, profilingLogger, mainDom, appCaches, databaseFactory, typeLoader, runtimeState, typeFinder, ioHelper, umbracoVersion, TestHelper.DbProviderFactoryCreator); // create the core runtime and have it compose itself - var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom);coreRuntime.Compose(composition); + var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, typeFinder); + coreRuntime.Compose(composition); // determine actual runtime level runtimeState.DetermineRuntimeLevel(databaseFactory, logger); @@ -276,7 +278,7 @@ namespace Umbraco.Tests.Runtimes composition.RegisterEssentials(logger, profiler, profilingLogger, mainDom, appCaches, databaseFactory, typeLoader, runtimeState, typeFinder, ioHelper, umbracoVersion, TestHelper.DbProviderFactoryCreator); // create the core runtime and have it compose itself - var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom); + var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, typeFinder); coreRuntime.Compose(composition); // get the components @@ -315,107 +317,14 @@ namespace Umbraco.Tests.Runtimes foreach (var result in resultGroup) { Console.WriteLine(); - Console.Write(ToText(result)); + Console.Write(result.ToText()); } Assert.AreEqual(0, results.Count); } - private static string ToText(ValidationResult result) - { - var text = new StringBuilder(); + - text.AppendLine($"{result.Severity}: {WordWrap(result.Message, 120)}"); - var target = result.ValidationTarget; - text.Append("\tsvce: "); - text.Append(target.ServiceName); - text.Append(target.DeclaringService.ServiceType); - if (!target.DeclaringService.ServiceName.IsNullOrWhiteSpace()) - { - text.Append(" '"); - text.Append(target.DeclaringService.ServiceName); - text.Append("'"); - } - - text.Append(" ("); - if (target.DeclaringService.Lifetime == null) - text.Append("Transient"); - else - text.Append(target.DeclaringService.Lifetime.ToString().TrimStart("LightInject.").TrimEnd("Lifetime")); - text.AppendLine(")"); - text.Append("\timpl: "); - text.Append(target.DeclaringService.ImplementingType); - text.AppendLine(); - text.Append("\tparm: "); - text.Append(target.Parameter); - text.AppendLine(); - - return text.ToString(); - } - - private static string WordWrap(string text, int width) - { - int pos, next; - var sb = new StringBuilder(); - var nl = Environment.NewLine; - - // Lucidity check - if (width < 1) - return text; - - // Parse each line of text - for (pos = 0; pos < text.Length; pos = next) - { - // Find end of line - var eol = text.IndexOf(nl, pos, StringComparison.Ordinal); - - if (eol == -1) - next = eol = text.Length; - else - next = eol + nl.Length; - - // Copy this line of text, breaking into smaller lines as needed - if (eol > pos) - { - do - { - var len = eol - pos; - - if (len > width) - len = BreakLine(text, pos, width); - - if (pos > 0) - sb.Append("\t\t"); - sb.Append(text, pos, len); - sb.Append(nl); - - // Trim whitespace following break - pos += len; - - while (pos < eol && char.IsWhiteSpace(text[pos])) - pos++; - - } while (eol > pos); - } - else sb.Append(nl); // Empty line - } - - return sb.ToString(); - } - - private static int BreakLine(string text, int pos, int max) - { - // Find last whitespace in line - var i = max - 1; - while (i >= 0 && !char.IsWhiteSpace(text[pos + i])) - i--; - if (i < 0) - return max; // No whitespace found; break at maximum length - // Find start of whitespace - while (i >= 0 && char.IsWhiteSpace(text[pos + i])) - i--; - // Return length of text before whitespace - return i + 1; - } + } } diff --git a/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs b/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs index 579ca104ea..24dcb229c9 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs @@ -33,7 +33,6 @@ namespace Umbraco.Tests.TestHelpers protected override void Compose() { base.Compose(); - base.Compose(); Composition.RegisterUnique(); Composition.RegisterUnique(); diff --git a/src/Umbraco.Tests/TestHelpers/TestHelper.cs b/src/Umbraco.Tests/TestHelpers/TestHelper.cs index 95e211db24..05d4de6e23 100644 --- a/src/Umbraco.Tests/TestHelpers/TestHelper.cs +++ b/src/Umbraco.Tests/TestHelpers/TestHelper.cs @@ -43,6 +43,11 @@ namespace Umbraco.Tests.TestHelpers private static TestHelperInternal _testHelperInternal = new TestHelperInternal(); private class TestHelperInternal : TestHelperBase { + public TestHelperInternal() : base(typeof(TestHelperInternal).Assembly) + { + + } + public override IDbProviderFactoryCreator DbProviderFactoryCreator { get; } = new UmbracoDbProviderFactoryCreator(Constants.DbProviderNames.SqlCe); public override IBulkSqlInsertProvider BulkSqlInsertProvider { get; } = new SqlCeBulkSqlInsertProvider(); diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 7dd0d6592b..0c68849ccf 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -120,7 +120,6 @@ - @@ -210,7 +209,6 @@ - diff --git a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreHostingEnvironment.cs b/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreHostingEnvironment.cs index 0da6950d04..00bc894b0d 100644 --- a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreHostingEnvironment.cs +++ b/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreHostingEnvironment.cs @@ -84,9 +84,10 @@ namespace Umbraco.Web.BackOffice.AspNetCore } } - + // TODO: This may need to take into account ~/ paths which means the ApplicationVirtualPath and is this the content root or web root? public string MapPath(string path) => Path.Combine(_webHostEnvironment.WebRootPath, path); + // TODO: Need to take into account 'root' here public string ToAbsolute(string virtualPath, string root) { if (Uri.TryCreate(virtualPath, UriKind.Absolute, out _)) diff --git a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreIpResolver.cs b/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreIpResolver.cs index 8f231191f2..cee43757d8 100644 --- a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreIpResolver.cs +++ b/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreIpResolver.cs @@ -3,7 +3,7 @@ using Umbraco.Net; namespace Umbraco.Web.BackOffice.AspNetCore { - internal class AspNetIpResolver : IIpResolver + public class AspNetIpResolver : IIpResolver { private readonly IHttpContextAccessor _httpContextAccessor; diff --git a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreSessionIdResolver.cs b/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreSessionIdResolver.cs index 5470516cf7..cafb02d367 100644 --- a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreSessionIdResolver.cs +++ b/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreSessionIdResolver.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Umbraco.Net; namespace Umbraco.Web.BackOffice.AspNetCore @@ -12,6 +13,18 @@ namespace Umbraco.Web.BackOffice.AspNetCore _httpContextAccessor = httpContextAccessor; } - public string SessionId => _httpContextAccessor?.HttpContext.Session?.Id; + + public string SessionId + { + get + { + var httpContext = _httpContextAccessor?.HttpContext; + // If session isn't enabled this will throw an exception so we check + var sessionFeature = httpContext?.Features.Get(); + return sessionFeature != null + ? httpContext?.Session?.Id + : "0"; + } + } } } diff --git a/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoBackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoBackOfficeServiceCollectionExtensions.cs index 8f94fba957..135ba90b97 100644 --- a/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoBackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoBackOfficeServiceCollectionExtensions.cs @@ -1,3 +1,6 @@ +using System; +using System.Data.Common; +using System.Reflection; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; @@ -7,20 +10,29 @@ using Umbraco.Composing; using Umbraco.Configuration; using Umbraco.Core; using Umbraco.Core.Cache; +using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Logging.Serilog; +using Umbraco.Core.Persistence; +using Umbraco.Core.Runtime; namespace Umbraco.Web.BackOffice.AspNetCore { + + public static class UmbracoBackOfficeServiceCollectionExtensions { + public static IServiceCollection AddUmbracoConfiguration(this IServiceCollection services) { var serviceProvider = services.BuildServiceProvider(); var configuration = serviceProvider.GetService(); + if (configuration == null) + throw new InvalidOperationException($"Could not resolve {typeof(IConfiguration)} from the container"); + var configsFactory = new AspNetCoreConfigsFactory(configuration); var configs = configsFactory.Create(); @@ -30,44 +42,102 @@ namespace Umbraco.Web.BackOffice.AspNetCore return services; } - public static IServiceCollection AddUmbracoBackOffice(this IServiceCollection services) + + /// + /// Adds the Umbraco Back Core requirements + /// + /// + /// + /// + /// Must be called after all services are added to the application because we are cross-wiring the container (currently) + /// + public static IServiceCollection AddUmbracoCore(this IServiceCollection services) + { + if (!UmbracoServiceProviderFactory.IsActive) + throw new InvalidOperationException("Ensure to add UseUmbraco() in your Program.cs after ConfigureWebHostDefaults to enable Umbraco's service provider factory"); + + var umbContainer = UmbracoServiceProviderFactory.UmbracoContainer; + + return services.AddUmbracoCore(umbContainer, Assembly.GetEntryAssembly()); + } + + public static IServiceCollection AddUmbracoCore(this IServiceCollection services, IRegister umbContainer, Assembly entryAssembly) { services.AddSingleton(); - var serviceProvider = services.BuildServiceProvider(); + CreateCompositionRoot(services); - var httpContextAccessor = serviceProvider.GetService(); - var webHostEnvironment = serviceProvider.GetService(); - var hostApplicationLifetime = serviceProvider.GetService(); + // TODO: Get rid of this 'Current' requirement + var globalSettings = Current.Configs.Global(); + var umbracoVersion = new UmbracoVersion(globalSettings); - var configs = serviceProvider.GetService(); + // TODO: Currently we are not passing in any TypeFinderConfig (with ITypeFinderSettings) which we should do, however + // this is not critical right now and would require loading in some config before boot time so just leaving this as-is for now. + var typeFinder = new TypeFinder(Current.Logger, new DefaultUmbracoAssemblyProvider(entryAssembly)); - services.CreateCompositionRoot( - httpContextAccessor, - webHostEnvironment, - hostApplicationLifetime, - configs); + var coreRuntime = GetCoreRuntime( + Current.Configs, + umbracoVersion, + Current.IOHelper, + Current.Logger, + Current.Profiler, + Current.HostingEnvironment, + Current.BackOfficeInfo, + typeFinder); + + var factory = coreRuntime.Boot(umbContainer); return services; } - - public static IServiceCollection CreateCompositionRoot( - this IServiceCollection services, - IHttpContextAccessor httpContextAccessor, - IWebHostEnvironment webHostEnvironment, - IHostApplicationLifetime hostApplicationLifetime, - Configs configs) + private static IRuntime GetCoreRuntime(Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, ILogger logger, + IProfiler profiler, Core.Hosting.IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo, + ITypeFinder typeFinder) { + var connectionStringConfig = configs.ConnectionStrings()[Constants.System.UmbracoConnectionName]; + var dbProviderFactoryCreator = new SqlServerDbProviderFactoryCreator( + connectionStringConfig?.ProviderName, + DbProviderFactories.GetFactory); + + // Determine if we should use the sql main dom or the default + var globalSettings = configs.Global(); + var connStrings = configs.ConnectionStrings(); + var appSettingMainDomLock = globalSettings.MainDomLock; + var mainDomLock = appSettingMainDomLock == "SqlMainDomLock" + ? (IMainDomLock)new SqlMainDomLock(logger, globalSettings, connStrings, dbProviderFactoryCreator) + : new MainDomSemaphoreLock(logger, hostingEnvironment); + + var mainDom = new MainDom(logger, hostingEnvironment, mainDomLock); + + var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetCoreBootPermissionsChecker(), + hostingEnvironment, backOfficeInfo, dbProviderFactoryCreator, mainDom, typeFinder); + + return coreRuntime; + } + + private static void CreateCompositionRoot(IServiceCollection services) + { + // TODO: This isn't the best to have to resolve the services now but to avoid this will + // require quite a lot of re-work. + var serviceProvider = services.BuildServiceProvider(); + + var httpContextAccessor = serviceProvider.GetRequiredService(); + var webHostEnvironment = serviceProvider.GetRequiredService(); + var hostApplicationLifetime = serviceProvider.GetRequiredService(); + + var configs = serviceProvider.GetService(); + if (configs == null) + throw new InvalidOperationException($"Could not resolve type {typeof(Configs)} from the container, ensure {nameof(AddUmbracoConfiguration)} is called before calling {nameof(AddUmbracoCore)}"); + var hostingSettings = configs.Hosting(); var coreDebug = configs.CoreDebug(); var globalSettings = configs.Global(); - var hostingEnvironment = new AspNetCoreHostingEnvironment(hostingSettings, webHostEnvironment, - httpContextAccessor, hostApplicationLifetime); + var hostingEnvironment = new AspNetCoreHostingEnvironment(hostingSettings, webHostEnvironment, httpContextAccessor, hostApplicationLifetime); var ioHelper = new IOHelper(hostingEnvironment, globalSettings); var logger = SerilogLogger.CreateWithDefaultConfiguration(hostingEnvironment, new AspNetCoreSessionIdResolver(httpContextAccessor), + // need to build a new service provider since the one already resolved above doesn't have the IRequestCache yet () => services.BuildServiceProvider().GetService(), coreDebug, ioHelper, new AspNetCoreMarchal()); @@ -75,8 +145,14 @@ namespace Umbraco.Web.BackOffice.AspNetCore var profiler = new LogProfiler(logger); Current.Initialize(logger, configs, ioHelper, hostingEnvironment, backOfficeInfo, profiler); + } - return services; + private class AspNetCoreBootPermissionsChecker : IUmbracoBootPermissionChecker + { + public void ThrowIfNotPermissions() + { + // nothing to check + } } } } diff --git a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj index 36238f1a6d..bd20769d45 100644 --- a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj +++ b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj @@ -10,6 +10,10 @@ + + + + diff --git a/src/Umbraco.Web.UI.NetCore/Program.cs b/src/Umbraco.Web.UI.NetCore/Program.cs index bbe78907f1..1151f16be8 100644 --- a/src/Umbraco.Web.UI.NetCore/Program.cs +++ b/src/Umbraco.Web.UI.NetCore/Program.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; - +using Umbraco.Core.Composing; namespace Umbraco.Web.UI.BackOffice { @@ -15,6 +15,7 @@ namespace Umbraco.Web.UI.BackOffice public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }) + .UseUmbraco(); } } diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index be81261d2a..444b9a835d 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -25,8 +25,8 @@ namespace Umbraco.Web.UI.BackOffice services.AddUmbracoConfiguration(); services.AddUmbracoRequest(); + services.AddUmbracoCore(); services.AddUmbracoWebsite(); - services.AddUmbracoBackOffice(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/src/Umbraco.Web/UmbracoApplication.cs b/src/Umbraco.Web/UmbracoApplication.cs index b7cdc1039a..9f24da95e3 100644 --- a/src/Umbraco.Web/UmbracoApplication.cs +++ b/src/Umbraco.Web/UmbracoApplication.cs @@ -36,7 +36,7 @@ namespace Umbraco.Web var requestCache = new HttpRequestAppCache(() => HttpContext.Current?.Items); var umbracoBootPermissionChecker = new AspNetUmbracoBootPermissionChecker(); - return new WebRuntime(configs, umbracoVersion, ioHelper, logger, profiler, hostingEnvironment, backOfficeInfo, dbProviderFactoryCreator, mainDom, requestCache, umbracoBootPermissionChecker); + return new WebRuntime(configs, umbracoVersion, ioHelper, logger, profiler, hostingEnvironment, backOfficeInfo, dbProviderFactoryCreator, mainDom, GetTypeFinder(), requestCache, umbracoBootPermissionChecker); } } } diff --git a/src/Umbraco.Web/UmbracoApplicationBase.cs b/src/Umbraco.Web/UmbracoApplicationBase.cs index fe4622d8fa..ce5f1304cd 100644 --- a/src/Umbraco.Web/UmbracoApplicationBase.cs +++ b/src/Umbraco.Web/UmbracoApplicationBase.cs @@ -24,12 +24,10 @@ namespace Umbraco.Web public abstract class UmbracoApplicationBase : HttpApplication { private IRuntime _runtime; - private IFactory _factory; protected UmbracoApplicationBase() { - if (!Umbraco.Composing.Current.IsInitialized) { var configFactory = new ConfigsFactory(); @@ -46,8 +44,8 @@ namespace Umbraco.Web var backOfficeInfo = new AspNetBackOfficeInfo(globalSettings, ioHelper, logger, configFactory.WebRoutingSettings); var profiler = GetWebProfiler(hostingEnvironment); Umbraco.Composing.Current.Initialize(logger, configs, ioHelper, hostingEnvironment, backOfficeInfo, profiler); + Logger = logger; } - } private IProfiler GetWebProfiler(IHostingEnvironment hostingEnvironment) @@ -67,13 +65,36 @@ namespace Umbraco.Web } protected UmbracoApplicationBase(ILogger logger, Configs configs, IIOHelper ioHelper, IProfiler profiler, IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo) - { + { if (!Umbraco.Composing.Current.IsInitialized) { + Logger = logger; Umbraco.Composing.Current.Initialize(logger, configs, ioHelper, hostingEnvironment, backOfficeInfo, profiler); } } + protected ILogger Logger { get; } + + /// + /// Gets a + /// + /// + protected virtual ITypeFinder GetTypeFinder() + // TODO: Currently we are not passing in any TypeFinderConfig (with ITypeFinderSettings) which we should do, however + // this is not critical right now and would require loading in some config before boot time so just leaving this as-is for now. + => new TypeFinder(Logger, new DefaultUmbracoAssemblyProvider( + // GetEntryAssembly was actually an exposed API by request of the aspnetcore team which works in aspnet core because a website + // in that case is essentially an exe. However in netframework there is no entry assembly, things don't really work that way since + // the process that is running the site is iisexpress, so this returns null. The best we can do is fallback to GetExecutingAssembly() + // which will just return Umbraco.Infrastructure (currently with netframework) and for our purposes that is OK. + // If you are curious... There is really no way to get the entry assembly in netframework without the hosting website having it's own + // code compiled for the global.asax which is the entry point. Because the default global.asax for umbraco websites is just a file inheriting + // from Umbraco.Web.UmbracoApplication, the global.asax file gets dynamically compiled into a DLL in the dynamic folder (we can get an instance + // of that, but this doesn't really help us) but the actually entry execution is still Umbraco.Web. So that is the 'highest' level entry point + // assembly we can get and we can only get that if we put this code into the WebRuntime since the executing assembly is the 'current' one. + // For this purpose, it doesn't matter if it's Umbraco.Web or Umbraco.Infrastructure since all assemblies are in that same path and we are + // getting rid of netframework. + Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly())); /// /// Gets a runtime. diff --git a/src/umbraco.sln b/src/umbraco.sln index 11098abdec..4150e17906 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -121,6 +121,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Web.UI.NetCore", "U EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Web.Website", "Umbraco.Web.Website\Umbraco.Web.Website.csproj", "{5A246D54-3109-4D2B-BE7D-FC0787D126AE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Tests.Integration", "Umbraco.Tests.Integration\Umbraco.Tests.Integration.csproj", "{D6319409-777A-4BD0-93ED-B2DFD805B32C}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Tests.Common", "Umbraco.Tests.Common\Umbraco.Tests.Common.csproj", "{A499779C-1B3B-48A8-B551-458E582E6E96}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Web.Common", "Umbraco.Web.Common\Umbraco.Web.Common.csproj", "{839D3048-9718-4907-BDE0-7CD63D364383}" @@ -193,6 +195,10 @@ Global {5A246D54-3109-4D2B-BE7D-FC0787D126AE}.Debug|Any CPU.Build.0 = Debug|Any CPU {5A246D54-3109-4D2B-BE7D-FC0787D126AE}.Release|Any CPU.ActiveCfg = Release|Any CPU {5A246D54-3109-4D2B-BE7D-FC0787D126AE}.Release|Any CPU.Build.0 = Release|Any CPU + {D6319409-777A-4BD0-93ED-B2DFD805B32C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6319409-777A-4BD0-93ED-B2DFD805B32C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6319409-777A-4BD0-93ED-B2DFD805B32C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6319409-777A-4BD0-93ED-B2DFD805B32C}.Release|Any CPU.Build.0 = Release|Any CPU {A499779C-1B3B-48A8-B551-458E582E6E96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A499779C-1B3B-48A8-B551-458E582E6E96}.Debug|Any CPU.Build.0 = Debug|Any CPU {A499779C-1B3B-48A8-B551-458E582E6E96}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -214,6 +220,7 @@ Global {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {C7311C00-2184-409B-B506-52A5FAEA8736} = {FD962632-184C-4005-A5F3-E705D92FC645} {FB5676ED-7A69-492C-B802-E7B24144C0FC} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} + {D6319409-777A-4BD0-93ED-B2DFD805B32C} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {A499779C-1B3B-48A8-B551-458E582E6E96} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution