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.

This commit is contained in:
Shannon
2020-03-13 18:44:58 +11:00
parent c9913f45a0
commit 9ded4c7ddb
13 changed files with 264 additions and 41 deletions

View File

@@ -0,0 +1,20 @@
using Microsoft.Extensions.Hosting;
namespace Umbraco.Core.Composing
{
/// <summary>
/// Extends the <see cref="IHostBuilder"/> to enable Umbraco to be used as the service container.
/// </summary>
public static class HostBuilderExtensions
{
/// <summary>
/// Assigns a custom service provider factory to use Umbraco's container
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
public static IHostBuilder UseUmbraco(this IHostBuilder builder)
{
return builder.UseServiceProviderFactory(new UmbracoServiceProviderFactory());
}
}
}

View File

@@ -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
{
/// <summary>
/// Creates the container.
/// </summary>
public static class RegisterFactory
{
/// <summary>
/// Creates a new <see cref="IRegister"/> based on an existing MSDI IServiceCollection
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
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

View File

@@ -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
{
/// <summary>
/// Used to create Umbraco's container and cross-wire it up before the applicaton starts
/// </summary>
public class UmbracoServiceProviderFactory : IServiceProviderFactory<IServiceContainer>
{
public UmbracoServiceProviderFactory(ServiceContainer container)
{
_container = new LightInjectContainer(container);
}
/// <summary>
/// Default ctor for use in Host Builder configuration
/// </summary>
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;
/// <summary>
/// When the empty ctor is used this returns if this factory is active
/// </summary>
public static bool IsActive { get; private set; }
/// <summary>
/// When the empty ctor is used this returns the created IRegister
/// </summary>
public static IRegister UmbracoContainer { get; private set; }
/// <summary>
/// Create the container with the required settings for aspnetcore3
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public IServiceContainer CreateBuilder(IServiceCollection services)
{
_services = services;
return _container.Container;
}
/// <summary>
/// This cross-wires the container just before the application calls "Configure"
/// </summary>
/// <param name="containerBuilder"></param>
/// <returns></returns>
public IServiceProvider CreateServiceProvider(IServiceContainer containerBuilder)
{
var provider = containerBuilder.CreateServiceProvider(_services);
return provider;
}
}
}

View File

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

View File

@@ -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<string, DbProviderFactory> _getFactory;
public SqlServerDbProviderFactoryCreator(string defaultProviderName, Func<string, DbProviderFactory> 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");
}
}
}

View File

@@ -10,6 +10,7 @@
<PackageReference Include="LightInject" Version="6.3.2" />
<PackageReference Include="LightInject.Annotation" Version="1.1.0" />
<PackageReference Include="LightInject.Microsoft.DependencyInjection" Version="3.3.0" />
<PackageReference Include="LightInject.Microsoft.Hosting" Version="1.2.0" />
<PackageReference Include="Markdown" Version="2.2.1" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.2" />
@@ -57,6 +58,9 @@
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Umbraco.Tests.Benchmarks</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Umbraco.Tests.Integration</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
@@ -67,8 +71,4 @@
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Composing\Microsoft\" />
</ItemGroup>
</Project>

View File

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

View File

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

View File

@@ -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<ISessionFeature>();
return sessionFeature != null
? _httpContextAccessor?.HttpContext?.Session?.Id
: "0";
}
}
}
}

View File

@@ -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)
/// <summary>
/// Adds the Umbraco Back Core requirements
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
/// <remarks>
/// Must be called after all services are added to the application because we are cross-wiring the container (currently)
/// </remarks>
public static IServiceCollection AddUmbracoCore(this IServiceCollection services)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
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<IRequestCache>(), coreDebug, ioHelper, new AspNetCoreMarchal());
var logger = SerilogLogger.CreateWithDefaultConfiguration(hostingEnvironment, new AspNetCoreSessionIdResolver(httpContextAccessor), () => services.BuildServiceProvider().GetService<IRequestCache>(), 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
}
}
}
}

View File

@@ -10,6 +10,10 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Umbraco.Configuration\Umbraco.Configuration.csproj" />
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />

View File

@@ -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<Startup>(); });
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); })
.UseUmbraco()
.UseSerilog();
}
}

View File

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