From 9ded4c7ddbfb378def319778cffaa110a1083412 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 13 Mar 2020 18:44:58 +1100 Subject: [PATCH] Wires up DI for cross wiring correctly ensuring that it occurs at the very end of ConfigureServices, updates tests accordingly, fixes a few other things. --- .../Composing/HostBuilderExtensions.cs | 20 +++++ .../Composing/RegisterFactory.cs | 16 +--- .../UmbracoServiceProviderFactory.cs | 72 ++++++++++++++++++ .../Persistence/IDbProviderFactoryCreator.cs | 2 +- .../SqlServerDbProviderFactoryCreator.cs | 55 ++++++++++++++ .../Umbraco.Infrastructure.csproj | 8 +- .../ContainerTests.cs | 10 ++- src/Umbraco.Tests.Integration/RuntimeTests.cs | 12 +-- .../AspNetCore/AspNetCoreSessionIdResolver.cs | 14 +++- ...coBackOfficeServiceCollectionExtensions.cs | 76 +++++++++++++++++-- .../Umbraco.Web.BackOffice.csproj | 4 + src/Umbraco.Web.UI.NetCore/Program.cs | 14 ++-- src/Umbraco.Web.UI.NetCore/Startup.cs | 2 +- 13 files changed, 264 insertions(+), 41 deletions(-) create mode 100644 src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs create mode 100644 src/Umbraco.Infrastructure/Composing/UmbracoServiceProviderFactory.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs 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/RegisterFactory.cs b/src/Umbraco.Infrastructure/Composing/RegisterFactory.cs index 9794de733a..835bd0b9a8 100644 --- a/src/Umbraco.Infrastructure/Composing/RegisterFactory.cs +++ b/src/Umbraco.Infrastructure/Composing/RegisterFactory.cs @@ -1,6 +1,7 @@ using LightInject; using LightInject.Microsoft.DependencyInjection; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using System; using System.Reflection; using Umbraco.Core.Composing.LightInject; @@ -8,24 +9,13 @@ using Umbraco.Core.Configuration; namespace Umbraco.Core.Composing { + /// /// Creates the container. /// public static class RegisterFactory { - /// - /// Creates a new based on an existing MSDI IServiceCollection - /// - /// - /// - public static IRegister CreateFrom(IServiceCollection services, out IServiceProvider serviceProvider) - { - var lightInjectContainer = new ServiceContainer(ContainerOptions.Default.WithMicrosoftSettings()); - serviceProvider = lightInjectContainer.CreateServiceProvider(services); - return new LightInjectContainer(lightInjectContainer); - } - - //TODO: The following can die when net framework is gone + //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 diff --git a/src/Umbraco.Infrastructure/Composing/UmbracoServiceProviderFactory.cs b/src/Umbraco.Infrastructure/Composing/UmbracoServiceProviderFactory.cs new file mode 100644 index 0000000000..1d79bf9837 --- /dev/null +++ b/src/Umbraco.Infrastructure/Composing/UmbracoServiceProviderFactory.cs @@ -0,0 +1,72 @@ +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); + } + + /// + /// Default ctor for use in Host Builder configuration + /// + public UmbracoServiceProviderFactory() + { + var container = new ServiceContainer(ContainerOptions.Default.Clone().WithMicrosoftSettings().WithAspNetCoreSettings()); + 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/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 797c8bb7fa..a5fea12a6b 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -10,6 +10,7 @@ + @@ -57,6 +58,9 @@ <_Parameter1>Umbraco.Tests.Benchmarks + + <_Parameter1>Umbraco.Tests.Integration + @@ -67,8 +71,4 @@ - - - - diff --git a/src/Umbraco.Tests.Integration/ContainerTests.cs b/src/Umbraco.Tests.Integration/ContainerTests.cs index b231e6def0..b85471b95a 100644 --- a/src/Umbraco.Tests.Integration/ContainerTests.cs +++ b/src/Umbraco.Tests.Integration/ContainerTests.cs @@ -1,5 +1,7 @@ using LightInject; +using LightInject.Microsoft.DependencyInjection; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Moq; using NUnit.Framework; using Umbraco.Core; @@ -26,7 +28,10 @@ namespace Umbraco.Tests.Integration var msdiServiceProvider = services.BuildServiceProvider(); // LightInject / Umbraco - var umbracoContainer = (LightInjectContainer)RegisterFactory.CreateFrom(services, out var lightInjectServiceProvider); + var container = new ServiceContainer(ContainerOptions.Default.Clone().WithMicrosoftSettings().WithAspNetCoreSettings()); + 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(); @@ -42,7 +47,8 @@ namespace Umbraco.Tests.Integration testHelper.AppCaches, umbracoDatabaseFactory, typeLoader, runtimeState, testHelper.GetTypeFinder(), testHelper.IOHelper, testHelper.GetUmbracoVersion(), dbProviderFactoryCreator); - // Resolve + // 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(); diff --git a/src/Umbraco.Tests.Integration/RuntimeTests.cs b/src/Umbraco.Tests.Integration/RuntimeTests.cs index 871520d1c6..32eb327127 100644 --- a/src/Umbraco.Tests.Integration/RuntimeTests.cs +++ b/src/Umbraco.Tests.Integration/RuntimeTests.cs @@ -10,6 +10,7 @@ using Umbraco.Core.Runtime; using Umbraco.Tests.Common; using Umbraco.Tests.Common.Composing; using Umbraco.Tests.Integration.Implementations; +using Umbraco.Web.BackOffice.AspNetCore; namespace Umbraco.Tests.Integration { @@ -19,20 +20,19 @@ namespace Umbraco.Tests.Integration [Test] public void BootCoreRuntime() { - // MSDI - var services = new ServiceCollection(); - // LightInject / Umbraco - var umbracoContainer = (LightInjectContainer)RegisterFactory.CreateFrom(services, out var lightInjectServiceProvider); + var container = new ServiceContainer(ContainerOptions.Default.Clone().WithMicrosoftSettings().WithAspNetCoreSettings()); + var serviceProviderFactory = new UmbracoServiceProviderFactory(container); + var umbracoContainer = serviceProviderFactory.GetContainer(); - // Dependencies needed for Core Runtime + // 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); diff --git a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreSessionIdResolver.cs b/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreSessionIdResolver.cs index 5470516cf7..ddd0a9b122 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,17 @@ namespace Umbraco.Web.BackOffice.AspNetCore _httpContextAccessor = httpContextAccessor; } - public string SessionId => _httpContextAccessor?.HttpContext.Session?.Id; + + public string SessionId + { + get + { + // If session isn't enabled this will throw an exception so we check + var sessionFeature = _httpContextAccessor?.HttpContext?.Features.Get(); + return sessionFeature != null + ? _httpContextAccessor?.HttpContext?.Session?.Id + : "0"; + } + } } } diff --git a/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoBackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoBackOfficeServiceCollectionExtensions.cs index 9441170ae4..0fbd45214d 100644 --- a/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoBackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoBackOfficeServiceCollectionExtensions.cs @@ -1,4 +1,6 @@ -using System.Configuration; +using System; +using System.Data.Common; +using System.Reflection; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -6,27 +8,82 @@ using Microsoft.Extensions.Hosting; using Umbraco.Composing; using Umbraco.Core; using Umbraco.Core.Cache; +using Umbraco.Core.Composing; using Umbraco.Core.Configuration; 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 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) { - - services.AddSingleton(); CreateCompositionRoot(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; + + // TODO: Get rid of this 'Current' requirement + var globalSettings = Current.Configs.Global(); + var umbracoVersion = new UmbracoVersion(globalSettings); + + var coreRuntime = GetCoreRuntime( + Current.Configs, + umbracoVersion, + Current.IOHelper, + Current.Logger, + Current.Profiler, + Current.HostingEnvironment, + Current.BackOfficeInfo); + + var factory = coreRuntime.Boot(umbContainer); + return services; } + private static IRuntime GetCoreRuntime(Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, ILogger logger, + IProfiler profiler, Core.Hosting.IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo) + { + 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 appSettingMainDomLock = configs.Global().MainDomLock; + var mainDomLock = appSettingMainDomLock == "SqlMainDomLock" + ? (IMainDomLock)new SqlMainDomLock(logger, configs, dbProviderFactoryCreator) + : new MainDomSemaphoreLock(logger, hostingEnvironment); + + var mainDom = new MainDom(logger, hostingEnvironment, mainDomLock); + + // 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(logger, new DefaultUmbracoAssemblyProvider(Assembly.GetEntryAssembly())); + + var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetCoreBootPermissionsChecker(), + hostingEnvironment, backOfficeInfo, dbProviderFactoryCreator, mainDom, typeFinder); + + return coreRuntime; + } private static void CreateCompositionRoot(IServiceCollection services) { @@ -43,7 +100,8 @@ namespace Umbraco.Web.BackOffice.AspNetCore var hostingEnvironment = new AspNetCoreHostingEnvironment(hostingSettings, webHostEnvironment, httpContextAccessor, hostApplicationLifetime); var ioHelper = new IOHelper(hostingEnvironment); - var logger = SerilogLogger.CreateWithDefaultConfiguration(hostingEnvironment, new AspNetCoreSessionIdResolver(httpContextAccessor), () => services.BuildServiceProvider().GetService(), coreDebug, ioHelper, new AspNetCoreMarchal()); + + var logger = SerilogLogger.CreateWithDefaultConfiguration(hostingEnvironment, new AspNetCoreSessionIdResolver(httpContextAccessor), () => services.BuildServiceProvider().GetService(), coreDebug, ioHelper, new AspNetCoreMarchal()); var configs = configFactory.Create(ioHelper, logger); var backOfficeInfo = new AspNetCoreBackOfficeInfo(configs.Global()); @@ -51,5 +109,13 @@ namespace Umbraco.Web.BackOffice.AspNetCore Current.Initialize(logger, configs, ioHelper, hostingEnvironment, backOfficeInfo, profiler); } + + 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 21eb1b6585..6b77cb5f93 100644 --- a/src/Umbraco.Web.UI.NetCore/Program.cs +++ b/src/Umbraco.Web.UI.NetCore/Program.cs @@ -1,11 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; +using Serilog; +using Umbraco.Core.Composing; namespace Umbraco.Web.UI.BackOffice { @@ -19,7 +15,9 @@ namespace Umbraco.Web.UI.BackOffice } public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }) + .UseUmbraco() + .UseSerilog(); } } diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index 8e4da28917..7e2d19ac02 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -19,8 +19,8 @@ namespace Umbraco.Web.UI.BackOffice // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { + services.AddUmbracoCore(); services.AddUmbracoWebsite(); - services.AddUmbracoBackOffice(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.