Move to Minimal Hosting Model in a backwards compatible way (#14656)

* Use minimal hosting model

* Make CoreRuntime backward compatible to the old hosting model

* Remove unneccessary methods from interface again

* Pushed the timeout for E2E test to 120 minutes instead of 60

* Updated the preview version from 6 to 7

* Explicitly call BootUmbracoAsync

* Add CreateUmbracoBuilder extension method

* Do not add IRuntime as hosted service when using WebApplication/WebApplicationBuilder

* Set StaticServiceProvider.Instance before booting

* Ensure Umbraco is booted and StaticServiceProvider.Instance is set before configuring middleware

* Do not enable static web assets on production environments

* Removed root namespace from viewImports

---------

Co-authored-by: Andreas Zerbst <andr317c@live.dk>
Co-authored-by: Ronald Barendse <ronald@barend.se>
This commit is contained in:
Bjarke Berg
2023-08-21 12:24:17 +02:00
committed by GitHub
parent 84cd7a163c
commit b1e42e334d
12 changed files with 149 additions and 124 deletions

View File

@@ -335,6 +335,7 @@ stages:
# E2E Tests
- job:
displayName: E2E Tests
timeoutInMinutes: 120
variables:
Umbraco__CMS__Unattended__UnattendedUserName: Playwright Test
Umbraco__CMS__Unattended__UnattendedUserPassword: UmbracoAcceptance123!

View File

@@ -4,7 +4,6 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Exceptions;
using Umbraco.Cms.Core.Hosting;
@@ -16,6 +15,7 @@ using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Extensions;
using ComponentCollection = Umbraco.Cms.Core.Composing.ComponentCollection;
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
using LogLevel = Umbraco.Cms.Core.Logging.LogLevel;
namespace Umbraco.Cms.Infrastructure.Runtime;
@@ -152,12 +152,6 @@ public class CoreRuntime : IRuntime
// Store token, so we can re-use this during restart
_cancellationToken = cancellationToken;
// Just in-case HostBuilder.ConfigureUmbracoDefaults() isn't used (e.g. upgrade from 9 and ignored advice).
if (StaticServiceProvider.Instance == null!)
{
StaticServiceProvider.Instance = _serviceProvider!;
}
if (isRestarting == false)
{
AppDomain.CurrentDomain.UnhandledException += (_, args)
@@ -170,8 +164,8 @@ public class CoreRuntime : IRuntime
AcquireMainDom();
// Notify for unattended install
await _eventAggregator.PublishAsync(new RuntimeUnattendedInstallNotification(), cancellationToken);
DetermineRuntimeLevel();
await _eventAggregator.PublishAsync(new RuntimeUnattendedInstallNotification(), cancellationToken);
DetermineRuntimeLevel();
if (!State.UmbracoCanBoot())
{
@@ -182,8 +176,7 @@ public class CoreRuntime : IRuntime
IApplicationShutdownRegistry hostingEnvironmentLifetime = _applicationShutdownRegistry;
if (hostingEnvironmentLifetime == null)
{
throw new InvalidOperationException(
$"An instance of {typeof(IApplicationShutdownRegistry)} could not be resolved from the container, ensure that one if registered in your runtime before calling {nameof(IRuntime)}.{nameof(StartAsync)}");
throw new InvalidOperationException($"An instance of {typeof(IApplicationShutdownRegistry)} could not be resolved from the container, ensure that one if registered in your runtime before calling {nameof(IRuntime)}.{nameof(StartAsync)}");
}
// If level is Run and reason is UpgradeMigrations, that means we need to perform an unattended upgrade
@@ -194,8 +187,7 @@ public class CoreRuntime : IRuntime
case RuntimeUnattendedUpgradeNotification.UpgradeResult.HasErrors:
if (State.BootFailedException == null)
{
throw new InvalidOperationException(
$"Unattended upgrade result was {RuntimeUnattendedUpgradeNotification.UpgradeResult.HasErrors} but no {nameof(BootFailedException)} was registered");
throw new InvalidOperationException($"Unattended upgrade result was {RuntimeUnattendedUpgradeNotification.UpgradeResult.HasErrors} but no {nameof(BootFailedException)} was registered");
}
// We cannot continue here, the exception will be rethrown by BootFailedMiddelware
@@ -210,33 +202,29 @@ public class CoreRuntime : IRuntime
}
// Initialize the components
_components.Initialize();
_components.Initialize();
await _eventAggregator.PublishAsync(
new UmbracoApplicationStartingNotification(State.Level, isRestarting),
cancellationToken);
await _eventAggregator.PublishAsync(new UmbracoApplicationStartingNotification(State.Level, isRestarting), cancellationToken);
if (isRestarting == false)
{
// Add application started and stopped notifications last (to ensure they're always published after starting)
_hostApplicationLifetime?.ApplicationStarted.Register(() =>
_eventAggregator.Publish(new UmbracoApplicationStartedNotification(false)));
_hostApplicationLifetime?.ApplicationStopped.Register(() =>
_eventAggregator.Publish(new UmbracoApplicationStoppedNotification(false)));
_hostApplicationLifetime?.ApplicationStarted.Register(() => _eventAggregator.Publish(new UmbracoApplicationStartedNotification(false)));
_hostApplicationLifetime?.ApplicationStopped.Register(() => _eventAggregator.Publish(new UmbracoApplicationStoppedNotification(false)));
}
}
private async Task StopAsync(CancellationToken cancellationToken, bool isRestarting)
{
_components.Terminate();
await _eventAggregator.PublishAsync(
new UmbracoApplicationStoppingNotification(isRestarting),
cancellationToken);
await _eventAggregator.PublishAsync(new UmbracoApplicationStoppingNotification(isRestarting), cancellationToken);
}
private void AcquireMainDom()
{
using DisposableTimer? timer = !_profilingLogger.IsEnabled(Core.Logging.LogLevel.Debug) ? null : _profilingLogger.DebugDuration<CoreRuntime>("Acquiring MainDom.", "Acquired.");
using DisposableTimer? timer = !_profilingLogger.IsEnabled(LogLevel.Debug)
? null
: _profilingLogger.DebugDuration<CoreRuntime>("Acquiring MainDom.", "Acquired.");
try
{
@@ -257,8 +245,9 @@ public class CoreRuntime : IRuntime
return;
}
using DisposableTimer? timer = !_profilingLogger.IsEnabled(Core.Logging.LogLevel.Debug) ? null :
_profilingLogger.DebugDuration<CoreRuntime>("Determining runtime level.", "Determined.");
using DisposableTimer? timer = !_profilingLogger.IsEnabled(LogLevel.Debug)
? null
: _profilingLogger.DebugDuration<CoreRuntime>("Determining runtime level.", "Determined.");
try
{
@@ -274,6 +263,7 @@ public class CoreRuntime : IRuntime
{
_logger.LogDebug("Configure database factory for upgrades.");
}
_databaseFactory.ConfigureForUpgrade();
}
}

View File

@@ -142,8 +142,6 @@ public static partial class UmbracoBuilderExtensions
sp,
sp.GetRequiredService<IApplicationDiscriminator>()));
builder.Services.AddHostedService(factory => factory.GetRequiredService<IRuntime>());
builder.Services.AddSingleton<DatabaseSchemaCreatorFactory>();
builder.Services.TryAddEnumerable(ServiceDescriptor
.Singleton<IDatabaseProviderMetadata, CustomConnectionStringDatabaseProviderMetadata>());

View File

@@ -9,6 +9,8 @@ using Serilog.Context;
using StackExchange.Profiling;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Exceptions;
using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Logging.Serilog.Enrichers;
@@ -30,7 +32,21 @@ public static class ApplicationBuilderExtensions
/// Configures and use services required for using Umbraco
/// </summary>
public static IUmbracoApplicationBuilder UseUmbraco(this IApplicationBuilder app)
=> new UmbracoApplicationBuilder(app);
{
// Ensure Umbraco is booted and StaticServiceProvider.Instance is set before continuing
IRuntimeState runtimeState = app.ApplicationServices.GetRequiredService<IRuntimeState>();
if (runtimeState.Level == RuntimeLevel.Unknown)
{
throw new BootFailedException("The runtime level is unknown, please make sure Umbraco is booted by adding `await app.BootUmbracoAsync();` just after `WebApplication app = builder.Build();` in your Program.cs file.");
}
if (StaticServiceProvider.Instance is null)
{
throw new BootFailedException("StaticServiceProvider.Instance is not set, please make sure ConfigureUmbracoDefaults() is added in your Program.cs file.");
}
return new UmbracoApplicationBuilder(app);
}
/// <summary>
/// Returns true if Umbraco <see cref="IRuntimeState" /> is greater than <see cref="RuntimeLevel.BootFailed" />

View File

@@ -0,0 +1,36 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
namespace Umbraco.Extensions;
/// <summary>
/// Extension methods for <see cref="WebApplicationBuilder" />.
/// </summary>
public static class WebApplicationBuilderExtensions
{
/// <summary>
/// Creates an <see cref="IUmbracoBuilder" /> and registers basic Umbraco services.
/// </summary>
/// <param name="builder">The builder.</param>
/// <returns>
/// The Umbraco builder.
/// </returns>
public static IUmbracoBuilder CreateUmbracoBuilder(this WebApplicationBuilder builder)
{
// Configure Umbraco defaults, but ignore decorated host builder and
// don't add runtime as hosted service (this is replaced by the explicit BootUmbracoAsync)
builder.Host.ConfigureUmbracoDefaults(false);
// Do not enable static web assets on production environments,
// because the files are already copied to the publish output folder.
if (builder.Configuration.GetRuntimeMode() != RuntimeMode.Production)
{
builder.WebHost.UseStaticWebAssets();
}
return builder.Services.AddUmbraco(builder.Environment, builder.Configuration);
}
}

View File

@@ -0,0 +1,31 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Extensions;
/// <summary>
/// Extension methods for <see cref="WebApplication" />.
/// </summary>
public static class WebApplicationExtensions
{
/// <summary>
/// Starts the <see cref="IRuntime" /> to ensure Umbraco is ready for middleware to be added.
/// </summary>
/// <param name="app">The application.</param>
/// <returns>
/// A <see cref="Task" /> representing the asynchronous operation.
/// </returns>
public static async Task BootUmbracoAsync(this WebApplication app)
{
// Set static IServiceProvider before booting
StaticServiceProvider.Instance = app.Services;
// Ensure the Umbraco runtime is started before middleware is added and stopped when performing a graceful shutdown
IRuntime umbracoRuntime = app.Services.GetRequiredService<IRuntime>();
CancellationTokenRegistration cancellationTokenRegistration = app.Lifetime.ApplicationStopping.Register((_, token) => umbracoRuntime.StopAsync(token), null);
await umbracoRuntime.StartAsync(cancellationTokenRegistration.Token);
}
}

View File

@@ -1,6 +1,8 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.Hosting;
// ReSharper disable once CheckNamespace
@@ -15,6 +17,9 @@ public static class HostBuilderExtensions
/// Configures an existing <see cref="IHostBuilder" /> with defaults for an Umbraco application.
/// </summary>
public static IHostBuilder ConfigureUmbracoDefaults(this IHostBuilder builder)
=> builder.ConfigureUmbracoDefaults(true);
internal static IHostBuilder ConfigureUmbracoDefaults(this IHostBuilder builder, bool addRuntimeHostedService)
{
#if DEBUG
builder.ConfigureAppConfiguration(config
@@ -26,10 +31,16 @@ public static class HostBuilderExtensions
#endif
builder.ConfigureLogging(x => x.ClearProviders());
if (addRuntimeHostedService)
{
// Add the Umbraco IRuntime as hosted service
builder.ConfigureServices(services => services.AddHostedService(factory => factory.GetRequiredService<IRuntime>()));
}
return new UmbracoHostBuilderDecorator(builder, OnHostBuilt);
}
// Runs before any IHostedService starts (including generic web host).
// Runs before any IHostedService starts (including generic web host)
private static void OnHostBuilt(IHost host) =>
StaticServiceProvider.Instance = host.Services;
}

View File

@@ -1,19 +1,36 @@
namespace Umbraco.Cms.Web.UI
{
public class Program
{
public static void Main(string[] args)
=> CreateHostBuilder(args)
.Build()
.Run();
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureUmbracoDefaults()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStaticWebAssets();
webBuilder.UseStartup<Startup>();
});
}
builder.CreateUmbracoBuilder()
.AddBackOffice()
.AddWebsite()
.AddDeliveryApi()
.AddComposers()
.Build();
WebApplication app = builder.Build();
await app.BootUmbracoAsync();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
#if (UseHttpsRedirect)
app.UseHttpsRedirection();
#endif
app.UseUmbraco()
.WithMiddleware(u =>
{
u.UseBackOffice();
u.UseWebsite();
})
.WithEndpoints(u =>
{
u.UseInstallerEndpoints();
u.UseBackOfficeEndpoints();
u.UseWebsiteEndpoints();
});
await app.RunAsync();

View File

@@ -1,70 +0,0 @@
namespace Umbraco.Cms.Web.UI
{
public class Startup
{
private readonly IWebHostEnvironment _env;
private readonly IConfiguration _config;
/// <summary>
/// Initializes a new instance of the <see cref="Startup" /> class.
/// </summary>
/// <param name="webHostEnvironment">The web hosting environment.</param>
/// <param name="config">The configuration.</param>
/// <remarks>
/// Only a few services are possible to be injected here https://github.com/dotnet/aspnetcore/issues/9337.
/// </remarks>
public Startup(IWebHostEnvironment webHostEnvironment, IConfiguration config)
{
_env = webHostEnvironment ?? throw new ArgumentNullException(nameof(webHostEnvironment));
_config = config ?? throw new ArgumentNullException(nameof(config));
}
/// <summary>
/// Configures the services.
/// </summary>
/// <param name="services">The services.</param>
/// <remarks>
/// This method gets called by the runtime. Use this method to add services to the container.
/// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940.
/// </remarks>
public void ConfigureServices(IServiceCollection services)
{
services.AddUmbraco(_env, _config)
.AddBackOffice()
.AddWebsite()
.AddDeliveryApi()
.AddComposers()
.Build();
}
/// <summary>
/// Configures the application.
/// </summary>
/// <param name="app">The application builder.</param>
/// <param name="env">The web hosting environment.</param>
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
#if (UseHttpsRedirect)
app.UseHttpsRedirection();
#endif
app.UseUmbraco()
.WithMiddleware(u =>
{
u.UseBackOffice();
u.UseWebsite();
})
.WithEndpoints(u =>
{
u.UseInstallerEndpoints();
u.UseBackOfficeEndpoints();
u.UseWebsiteEndpoints();
});
}
}
}

View File

@@ -1,5 +1,4 @@
@using Umbraco.Extensions
@using Umbraco.Cms.Web.UI
@using Umbraco.Cms.Web.Common.PublishedModels
@using Umbraco.Cms.Web.Common.Views
@using Umbraco.Cms.Core.Models.PublishedContent

View File

@@ -19,10 +19,6 @@
<Link>UmbracoProject\Program.cs</Link>
<PackagePath>UmbracoProject</PackagePath>
</Content>
<Content Include="..\src\Umbraco.Web.UI\Startup.cs">
<Link>UmbracoProject\Startup.cs</Link>
<PackagePath>UmbracoProject</PackagePath>
</Content>
<Content Include="..\src\Umbraco.Web.UI\Views\Partials\blocklist\**">
<Link>UmbracoProject\Views\Partials\blocklist\%(RecursiveDir)%(Filename)%(Extension)</Link>
<PackagePath>UmbracoProject\Views\Partials\blocklist</PackagePath>

View File

@@ -2,7 +2,7 @@
## Build
############################################
FROM mcr.microsoft.com/dotnet/nightly/sdk:8.0.100-preview.6-jammy AS build
FROM mcr.microsoft.com/dotnet/nightly/sdk:8.0.100-preview.7-jammy AS build
COPY nuget.config .