Moved files from backoffice into Common + Introduced AspNetCoreComponent to invoke the Umbraco Application Init event

This commit is contained in:
Bjarke Berg
2020-03-27 11:39:17 +01:00
parent fe88662f48
commit 681a25b861
26 changed files with 134 additions and 102 deletions

View File

@@ -0,0 +1,59 @@
using System;
using System.Collections.Concurrent;
using System.Threading;
using Microsoft.Extensions.Hosting;
using Umbraco.Core;
using Umbraco.Core.Hosting;
namespace Umbraco.Web.Common.AspNetCore
{
public class AspNetCoreApplicationShutdownRegistry : IApplicationShutdownRegistry
{
private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly ConcurrentDictionary<IRegisteredObject, RegisteredObjectWrapper> _registeredObjects =
new ConcurrentDictionary<IRegisteredObject, RegisteredObjectWrapper>();
public AspNetCoreApplicationShutdownRegistry(IHostApplicationLifetime hostApplicationLifetime)
{
_hostApplicationLifetime = hostApplicationLifetime;
}
public void RegisterObject(IRegisteredObject registeredObject)
{
var wrapped = new RegisteredObjectWrapper(registeredObject);
if (!_registeredObjects.TryAdd(registeredObject, wrapped))
{
throw new InvalidOperationException("Could not register object");
}
var cancellationTokenRegistration = _hostApplicationLifetime.ApplicationStopping.Register(() => wrapped.Stop(true));
wrapped.CancellationTokenRegistration = cancellationTokenRegistration;
}
public void UnregisterObject(IRegisteredObject registeredObject)
{
if (_registeredObjects.TryGetValue(registeredObject, out var wrapped))
{
wrapped.CancellationTokenRegistration.Unregister();
}
}
private class RegisteredObjectWrapper
{
private readonly IRegisteredObject _inner;
public RegisteredObjectWrapper(IRegisteredObject inner)
{
_inner = inner;
}
public CancellationTokenRegistration CancellationTokenRegistration { get; set; }
public void Stop(bool immediate)
{
_inner.Stop(immediate);
}
}
}
}

View File

@@ -0,0 +1,16 @@
using Umbraco.Core;
using Umbraco.Core.Configuration;
namespace Umbraco.Web.Common.AspNetCore
{
public class AspNetCoreBackOfficeInfo : IBackOfficeInfo
{
public AspNetCoreBackOfficeInfo(IGlobalSettings globalSettings)
{
GetAbsoluteUrl = globalSettings.UmbracoPath;
}
public string GetAbsoluteUrl { get; }
}
}

View File

@@ -0,0 +1,110 @@
using System;
using System.Globalization;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Umbraco.Core;
using Umbraco.Core.Configuration;
namespace Umbraco.Web.Common.AspNetCore
{
public class AspNetCoreHostingEnvironment : Umbraco.Core.Hosting.IHostingEnvironment
{
private readonly IHostingSettings _hostingSettings;
private readonly IWebHostEnvironment _webHostEnvironment;
private readonly IHttpContextAccessor _httpContextAccessor;
private string _localTempPath;
public AspNetCoreHostingEnvironment(IHostingSettings hostingSettings, IWebHostEnvironment webHostEnvironment, IHttpContextAccessor httpContextAccessor)
{
_hostingSettings = hostingSettings ?? throw new ArgumentNullException(nameof(hostingSettings));
_webHostEnvironment = webHostEnvironment;
_httpContextAccessor = httpContextAccessor;
SiteName = webHostEnvironment.ApplicationName;
ApplicationId = AppDomain.CurrentDomain.Id.ToString();
ApplicationPhysicalPath = webHostEnvironment.ContentRootPath;
ApplicationVirtualPath = "/"; //TODO how to find this, This is a server thing, not application thing.
IISVersion = new Version(0, 0); // TODO not necessary IIS
IsDebugMode = _hostingSettings.DebugMode;
}
public bool IsHosted { get; } = true;
public string SiteName { get; }
public string ApplicationId { get; }
public string ApplicationPhysicalPath { get; }
public string ApplicationVirtualPath { get; }
public bool IsDebugMode { get; }
public Version IISVersion { get; }
public string LocalTempPath
{
get
{
if (_localTempPath != null)
return _localTempPath;
switch (_hostingSettings.LocalTempStorageLocation)
{
case LocalTempStorage.AspNetTemp:
// TODO: I don't think this is correct? but also we probably can remove AspNetTemp as an option entirely
// since this is legacy and we shouldn't use it
return _localTempPath = System.IO.Path.Combine(Path.GetTempPath(), ApplicationId, "UmbracoData");
case LocalTempStorage.EnvironmentTemp:
// environment temp is unique, we need a folder per site
// use a hash
// combine site name and application id
// site name is a Guid on Cloud
// application id is eg /LM/W3SVC/123456/ROOT
// the combination is unique on one server
// and, if a site moves from worker A to B and then back to A...
// hopefully it gets a new Guid or new application id?
var hashString = SiteName + "::" + ApplicationId;
var hash = hashString.GenerateHash();
var siteTemp = System.IO.Path.Combine(Environment.ExpandEnvironmentVariables("%temp%"), "UmbracoData", hash);
return _localTempPath = siteTemp;
//case LocalTempStorage.Default:
//case LocalTempStorage.Unknown:
default:
return _localTempPath = MapPath("~/App_Data/TEMP");
}
}
}
public string MapPath(string path)
{
var newPath = path.TrimStart('~', '/').Replace('/', Path.DirectorySeparatorChar);
return Path.Combine(_webHostEnvironment.WebRootPath, newPath);
}
// TODO: Need to take into account 'root' here
public string ToAbsolute(string virtualPath, string root)
{
if (Uri.TryCreate(virtualPath, UriKind.Absolute, out _))
{
return virtualPath;
}
var segment = new PathString(virtualPath.Substring(1));
var applicationPath = _httpContextAccessor.HttpContext.Request.PathBase;
return applicationPath.Add(segment).Value;
}
}
}

View File

@@ -0,0 +1,17 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Net;
namespace Umbraco.Web.Common.AspNetCore
{
public class AspNetIpResolver : IIpResolver
{
private readonly IHttpContextAccessor _httpContextAccessor;
public AspNetIpResolver(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public string GetCurrentRequestIpAddress() => _httpContextAccessor?.HttpContext?.Connection?.RemoteIpAddress?.ToString() ?? string.Empty;
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Runtime.InteropServices;
using Umbraco.Core.Diagnostics;
namespace Umbraco.Web.Common.AspNetCore
{
public class AspNetCoreMarchal : IMarchal
{
// This thing is not available in net standard, but exists in both .Net 4 and .Net Core 3
public IntPtr GetExceptionPointers() => Marshal.GetExceptionPointers();
}
}

View File

@@ -0,0 +1,30 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Umbraco.Net;
namespace Umbraco.Web.Common.AspNetCore
{
internal class AspNetCoreSessionIdResolver : ISessionIdResolver
{
private readonly IHttpContextAccessor _httpContextAccessor;
public AspNetCoreSessionIdResolver(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
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<ISessionFeature>();
return sessionFeature != null
? httpContext?.Session?.Id
: "0";
}
}
}
}

View File

@@ -15,11 +15,6 @@ namespace Umbraco.Web.Common.AspNetCore
{
_httpContextAccessor = httpContextAccessor;
_hostApplicationLifetime = hostApplicationLifetime;
hostApplicationLifetime.ApplicationStarted.Register(() =>
{
ApplicationInit?.Invoke(this, EventArgs.Empty);
});
}
public bool IsRestarting { get; set; }
@@ -39,6 +34,10 @@ namespace Umbraco.Web.Common.AspNetCore
_hostApplicationLifetime.StopApplication();
}
public void InvokeApplicationInit()
{
ApplicationInit?.Invoke(this, EventArgs.Empty);
}
public event EventHandler ApplicationInit;
}
}

View File

@@ -0,0 +1,186 @@
using System;
using System.Data.Common;
using System.Reflection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Umbraco.Composing;
using Umbraco.Configuration;
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;
using Umbraco.Web.Common.Runtime.Profiler;
namespace Umbraco.Web.Common.AspNetCore
{
public static class UmbracoCoreServiceCollectionExtensions
{
/// <summary>
/// Adds the Umbraco Configuration requirements
/// </summary>
/// <param name="services"></param>
/// <param name="configuration"></param>
/// <returns></returns>
public static IServiceCollection AddUmbracoConfiguration(this IServiceCollection services, IConfiguration configuration)
{
if (configuration == null) throw new ArgumentNullException(nameof(configuration));
var configsFactory = new AspNetCoreConfigsFactory(configuration);
var configs = configsFactory.Create();
services.AddSingleton(configs);
return services;
}
/// <summary>
/// Adds the Umbraco Back Core requirements
/// </summary>
/// <param name="services"></param>
/// <param name="webHostEnvironment"></param>
/// <returns></returns>
public static IServiceCollection AddUmbracoCore(this IServiceCollection services, IWebHostEnvironment webHostEnvironment)
{
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(webHostEnvironment, umbContainer, Assembly.GetEntryAssembly());
}
/// <summary>
/// Adds the Umbraco Back Core requirements
/// </summary>
/// <param name="services"></param>
/// <param name="webHostEnvironment"></param>
/// <param name="umbContainer"></param>
/// <param name="entryAssembly"></param>
/// <returns></returns>
public static IServiceCollection AddUmbracoCore(this IServiceCollection services, IWebHostEnvironment webHostEnvironment, IRegister umbContainer, Assembly entryAssembly)
{
if (services is null) throw new ArgumentNullException(nameof(services));
var container = umbContainer;
if (container is null) throw new ArgumentNullException(nameof(container));
if (entryAssembly is null) throw new ArgumentNullException(nameof(entryAssembly));
// Special case! The generic host adds a few default services but we need to manually add this one here NOW because
// we resolve it before the host finishes configuring in the call to CreateCompositionRoot
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
CreateCompositionRoot(services, webHostEnvironment, out var logger, out var configs, out var ioHelper, out var hostingEnvironment, out var backOfficeInfo, out var profiler);
var globalSettings = configs.Global();
var umbracoVersion = new UmbracoVersion(globalSettings);
// 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(entryAssembly));
var coreRuntime = GetCoreRuntime(
configs,
umbracoVersion,
ioHelper,
logger,
profiler,
hostingEnvironment,
backOfficeInfo,
typeFinder);
var factory = coreRuntime.Configure(container);
return services;
}
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, 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, IWebHostEnvironment webHostEnvironment,
out ILogger logger, out Configs configs, out IIOHelper ioHelper, out Core.Hosting.IHostingEnvironment hostingEnvironment,
out IBackOfficeInfo backOfficeInfo, out IProfiler profiler)
{
// TODO: We need to avoid this, surely there's a way? See ContainerTests.BuildServiceProvider_Before_Host_Is_Configured
var serviceProvider = services.BuildServiceProvider();
var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
configs = serviceProvider.GetService<Configs>();
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();
hostingEnvironment = new AspNetCoreHostingEnvironment(hostingSettings, webHostEnvironment, httpContextAccessor);
ioHelper = new IOHelper(hostingEnvironment, globalSettings);
logger = SerilogLogger.CreateWithDefaultConfiguration(hostingEnvironment,
new AspNetCoreSessionIdResolver(httpContextAccessor),
// TODO: We need to avoid this, surely there's a way? See ContainerTests.BuildServiceProvider_Before_Host_Is_Configured
() => services.BuildServiceProvider().GetService<IRequestCache>(), coreDebug, ioHelper,
new AspNetCoreMarchal());
backOfficeInfo = new AspNetCoreBackOfficeInfo(globalSettings);
profiler = GetWebProfiler(hostingEnvironment, httpContextAccessor);
Current.Initialize(logger, configs, ioHelper, hostingEnvironment, backOfficeInfo, profiler);
}
private static IProfiler GetWebProfiler(Umbraco.Core.Hosting.IHostingEnvironment hostingEnvironment, IHttpContextAccessor httpContextAccessor)
{
// create and start asap to profile boot
if (!hostingEnvironment.IsDebugMode)
{
// should let it be null, that's how MiniProfiler is meant to work,
// but our own IProfiler expects an instance so let's get one
return new VoidProfiler();
}
var webProfiler = new WebProfiler(httpContextAccessor);
webProfiler.Start();
return webProfiler;
}
private class AspNetCoreBootPermissionsChecker : IUmbracoBootPermissionChecker
{
public void ThrowIfNotPermissions()
{
// nothing to check
}
}
}
}

View File

@@ -0,0 +1,11 @@
using System;
using Microsoft.AspNetCore.Http;
namespace Umbraco.Web.Common.Lifetime
{
public interface IUmbracoRequestLifetime
{
event EventHandler<HttpContext> RequestStart;
event EventHandler<HttpContext> RequestEnd;
}
}

View File

@@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Http;
namespace Umbraco.Web.Common.Lifetime
{
public interface IUmbracoRequestLifetimeManager
{
void InitRequest(HttpContext context);
void EndRequest(HttpContext context);
}
}

View File

@@ -0,0 +1,21 @@
using System;
using Microsoft.AspNetCore.Http;
namespace Umbraco.Web.Common.Lifetime
{
public class UmbracoRequestLifetime : IUmbracoRequestLifetime, IUmbracoRequestLifetimeManager
{
public event EventHandler<HttpContext> RequestStart;
public event EventHandler<HttpContext> RequestEnd;
public void InitRequest(HttpContext context)
{
RequestStart?.Invoke(this, context);
}
public void EndRequest(HttpContext context)
{
RequestEnd?.Invoke(this, context);
}
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Umbraco.Web.Common.Lifetime;
namespace Umbraco.Web.Common.Middleware
{
@@ -22,31 +23,4 @@ namespace Umbraco.Web.Common.Middleware
}
}
public interface IUmbracoRequestLifetime
{
event EventHandler<HttpContext> RequestStart;
event EventHandler<HttpContext> RequestEnd;
}
public class UmbracoRequestLifetime : IUmbracoRequestLifetime, IUmbracoRequestLifetimeManager
{
public event EventHandler<HttpContext> RequestStart;
public event EventHandler<HttpContext> RequestEnd;
public void InitRequest(HttpContext context)
{
RequestStart?.Invoke(this, context);
}
public void EndRequest(HttpContext context)
{
RequestEnd?.Invoke(this, context);
}
}
public interface IUmbracoRequestLifetimeManager
{
void InitRequest(HttpContext context);
void EndRequest(HttpContext context);
}
}

View File

@@ -0,0 +1,29 @@
using Microsoft.Extensions.Hosting;
using Umbraco.Core.Composing;
using Umbraco.Net;
namespace Umbraco.Web.Common.Runtime
{
public sealed class AspNetCoreComponent : IComponent
{
private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly IUmbracoApplicationLifetime _umbracoApplicationLifetime;
public AspNetCoreComponent(IHostApplicationLifetime hostApplicationLifetime, IUmbracoApplicationLifetime umbracoApplicationLifetime)
{
_hostApplicationLifetime = hostApplicationLifetime;
_umbracoApplicationLifetime = umbracoApplicationLifetime;
}
public void Initialize()
{
_hostApplicationLifetime.ApplicationStarted.Register(() => {
_umbracoApplicationLifetime.InvokeApplicationInit();
});
}
public void Terminate()
{
}
}
}

View File

@@ -0,0 +1,37 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Hosting;
using Umbraco.Net;
using Umbraco.Core.Runtime;
using Umbraco.Web.Common.AspNetCore;
using Umbraco.Web.Common.Lifetime;
namespace Umbraco.Web.Common.Runtime
{
/// <summary>
/// Adds/replaces AspNetCore specific services
/// </summary>
[ComposeBefore(typeof(ICoreComposer))]
[ComposeAfter(typeof(CoreInitialComposer))]
public class AspNetCoreComposer : ComponentComposer<AspNetCoreComponent>, IComposer
{
public new void Compose(Composition composition)
{
base.Compose(composition);
// AspNetCore specific services
composition.RegisterUnique<IHttpContextAccessor, HttpContextAccessor>();
// Our own netcore implementations
composition.RegisterUnique<IUmbracoApplicationLifetime, AspNetCoreUmbracoApplicationLifetime>();
composition.RegisterUnique<IApplicationShutdownRegistry, AspNetCoreApplicationShutdownRegistry>();
// The umbraco request lifetime
var umbracoRequestLifetime = new UmbracoRequestLifetime();
composition.RegisterUnique<IUmbracoRequestLifetimeManager>(factory => umbracoRequestLifetime);
composition.RegisterUnique<IUmbracoRequestLifetime>(factory => umbracoRequestLifetime);
composition.RegisterUnique<IUmbracoApplicationLifetime, AspNetCoreUmbracoApplicationLifetime>();
}
}
}

View File

@@ -1,18 +0,0 @@
using Umbraco.Core.Composing;
namespace Umbraco.Web.Common.Runtime.Profiler
{
public sealed class WebInitialComponent : IComponent
{
public void Initialize()
{
}
public void Terminate()
{
}
}
}

View File

@@ -1,24 +0,0 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Net;
using Umbraco.Web.Common.AspNetCore;
using Umbraco.Web.Common.Middleware;
namespace Umbraco.Web.Common.Runtime.Profiler
{
public class WebInitialComposer : ComponentComposer<WebInitialComponent>, ICoreComposer
{
public override void Compose(Composition composition)
{
base.Compose(composition);
var umbracoRequestLifetime = new UmbracoRequestLifetime();
composition.RegisterUnique<IUmbracoRequestLifetimeManager>(factory => umbracoRequestLifetime);
composition.RegisterUnique<IUmbracoRequestLifetime>(factory => umbracoRequestLifetime);
composition.RegisterUnique<IUmbracoApplicationLifetime, AspNetCoreUmbracoApplicationLifetime>();
}
}
}

View File

@@ -22,10 +22,15 @@ namespace Umbraco.Web.Common.Runtime.Profiler
// create our own provider, which can provide a profiler even during boot
_provider = new WebProfilerProvider();
_httpContextAccessor = httpContextAccessor;
MiniProfiler.DefaultOptions.ProfilerProvider = _provider;
}
public string Render() => MiniProfiler.Current
.RenderIncludes(_httpContextAccessor.HttpContext, RenderPosition.Right).ToString();
public string Render()
{
return MiniProfiler.Current
.RenderIncludes(_httpContextAccessor.HttpContext, RenderPosition.Right).ToString();
}
public IDisposable Step(string name) => MiniProfiler.Current?.Step(name);

View File

@@ -2,6 +2,7 @@
using Umbraco.Core.Composing;
using Umbraco.Core.Logging;
using Umbraco.Net;
using Umbraco.Web.Common.Lifetime;
using Umbraco.Web.Common.Middleware;
namespace Umbraco.Web.Common.Runtime.Profiler

View File

@@ -11,6 +11,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Umbraco.Configuration\Umbraco.Configuration.csproj" />
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />
<ProjectReference Include="..\Umbraco.Infrastructure\Umbraco.Infrastructure.csproj" />
</ItemGroup>