From c65f18454577ca596c3da9b49478c40ffc46f2d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Thierry=20K=C3=A9chichian?= Date: Sat, 2 Dec 2023 06:57:26 +0100 Subject: [PATCH] Startup Async Configure (#14801) --- .../OrchardCore.Localization/Startup.cs | 7 ++-- .../Modules/Builder/OrchardCoreBuilder.cs | 41 +++++++++++++++++++ .../Modules/Builder/StartupActions.cs | 10 ++++- .../Modules/Builder/StartupActionsStartup.cs | 14 +++++++ .../Modules/IAsyncStartup.cs | 21 ++++++++++ .../Modules/StartupBase.cs | 6 ++- .../Extensions/ShellPipelineExtensions.cs | 41 +++++++++++++------ 7 files changed, 122 insertions(+), 18 deletions(-) create mode 100644 src/OrchardCore/OrchardCore.Abstractions/Modules/IAsyncStartup.cs diff --git a/src/OrchardCore.Modules/OrchardCore.Localization/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Localization/Startup.cs index 8c825301c3d..73b06eab34c 100644 --- a/src/OrchardCore.Modules/OrchardCore.Localization/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Localization/Startup.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; @@ -41,12 +42,12 @@ public override void ConfigureServices(IServiceCollection services) } /// - public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) + public override async ValueTask ConfigureAsync(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) { var localizationService = serviceProvider.GetService(); - var defaultCulture = localizationService.GetDefaultCultureAsync().GetAwaiter().GetResult(); - var supportedCultures = localizationService.GetSupportedCulturesAsync().GetAwaiter().GetResult(); + var defaultCulture = await localizationService.GetDefaultCultureAsync(); + var supportedCultures = await localizationService.GetSupportedCulturesAsync(); var localizationOptions = serviceProvider.GetService>().Value; var ignoreSystemSettings = serviceProvider.GetService>().Value.IgnoreSystemSettings; diff --git a/src/OrchardCore/OrchardCore.Abstractions/Modules/Builder/OrchardCoreBuilder.cs b/src/OrchardCore/OrchardCore.Abstractions/Modules/Builder/OrchardCoreBuilder.cs index ba41209dfc6..b3c679717ea 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/Modules/Builder/OrchardCoreBuilder.cs +++ b/src/OrchardCore/OrchardCore.Abstractions/Modules/Builder/OrchardCoreBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using OrchardCore.Environment.Shell.Builders; @@ -97,6 +98,46 @@ public OrchardCoreBuilder Configure(Action configure, int o return Configure((app, routes, sp) => configure(app), order); } + /// + /// This async action gets called for each tenant. Use this method to configure the tenant pipeline. + /// + /// The async action to execute when configuring the request's pipeline for a tenant. + /// The order of the action to execute. Lower values will be executed first. + public OrchardCoreBuilder Configure(Func configureAsync, int order = 0) + { + if (!_actions.TryGetValue(order, out var actions)) + { + actions = _actions[order] = new StartupActions(order); + + ApplicationServices.AddTransient(sp => new StartupActionsStartup( + sp.GetRequiredService(), actions, order)); + } + + actions.AsyncConfigureActions.Add(configureAsync); + + return this; + } + + /// + /// This async action gets called for each tenant. Use this method to configure the tenant pipeline. + /// + /// The async action to execute when configuring the request's pipeline for a tenant. + /// The order of the action to execute. Lower values will be executed first. + public OrchardCoreBuilder Configure(Func configureAsync, int order = 0) + { + return Configure((app, routes, sp) => configureAsync(app, routes), order); + } + + /// + /// This async action gets called for each tenant. Use this method to configure the tenant pipeline. + /// + /// The async action to execute when configuring the request's pipeline for a tenant. + /// The order of the action to execute. Lower values will be executed first. + public OrchardCoreBuilder Configure(Func configureAsync, int order = 0) + { + return Configure((app, routes, sp) => configureAsync(app), order); + } + public OrchardCoreBuilder EnableFeature(string id) { return ConfigureServices(services => diff --git a/src/OrchardCore/OrchardCore.Abstractions/Modules/Builder/StartupActions.cs b/src/OrchardCore/OrchardCore.Abstractions/Modules/Builder/StartupActions.cs index 7fd53da5a12..bb34dfafe2b 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/Modules/Builder/StartupActions.cs +++ b/src/OrchardCore/OrchardCore.Abstractions/Modules/Builder/StartupActions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; @@ -14,8 +15,13 @@ public StartupActions(int order) public int Order { get; } - public ICollection> ConfigureServicesActions { get; } = new List>(); + public ICollection> ConfigureServicesActions { get; } = + new List>(); - public ICollection> ConfigureActions { get; } = new List>(); + public ICollection> ConfigureActions { get; } = + new List>(); + + public ICollection> AsyncConfigureActions { get; } = + new List>(); } } diff --git a/src/OrchardCore/OrchardCore.Abstractions/Modules/Builder/StartupActionsStartup.cs b/src/OrchardCore/OrchardCore.Abstractions/Modules/Builder/StartupActionsStartup.cs index 603ecbe7e83..20273aaf0d9 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/Modules/Builder/StartupActionsStartup.cs +++ b/src/OrchardCore/OrchardCore.Abstractions/Modules/Builder/StartupActionsStartup.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; @@ -37,5 +38,18 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro configure?.Invoke(app, routes, serviceProvider); } } + + public override async ValueTask ConfigureAsync(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) + { + foreach (var asyncConfigure in _actions.AsyncConfigureActions) + { + if (asyncConfigure is null) + { + continue; + } + + await asyncConfigure(app, routes, serviceProvider); + } + } } } diff --git a/src/OrchardCore/OrchardCore.Abstractions/Modules/IAsyncStartup.cs b/src/OrchardCore/OrchardCore.Abstractions/Modules/IAsyncStartup.cs new file mode 100644 index 00000000000..24519cea75e --- /dev/null +++ b/src/OrchardCore/OrchardCore.Abstractions/Modules/IAsyncStartup.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; + +namespace OrchardCore.Modules +{ + /// + /// An implementation of this interface allows to configure asynchronously the tenant pipeline. + /// + public interface IAsyncStartup + { + /// + /// This method gets called by the runtime. Use this method to configure the tenant pipeline. + /// + /// + /// + /// + ValueTask ConfigureAsync(IApplicationBuilder builder, IEndpointRouteBuilder routes, IServiceProvider serviceProvider); + } +} diff --git a/src/OrchardCore/OrchardCore.Abstractions/Modules/StartupBase.cs b/src/OrchardCore/OrchardCore.Abstractions/Modules/StartupBase.cs index e24c6972224..97087c5dbbd 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/Modules/StartupBase.cs +++ b/src/OrchardCore/OrchardCore.Abstractions/Modules/StartupBase.cs @@ -1,11 +1,12 @@ using System; +using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; namespace OrchardCore.Modules { - public abstract class StartupBase : IStartup + public abstract class StartupBase : IStartup, IAsyncStartup { /// public virtual int Order { get; } = 0; @@ -22,5 +23,8 @@ public virtual void ConfigureServices(IServiceCollection services) public virtual void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) { } + + /// + public virtual ValueTask ConfigureAsync(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) => default; } } diff --git a/src/OrchardCore/OrchardCore/Modules/Extensions/ShellPipelineExtensions.cs b/src/OrchardCore/OrchardCore/Modules/Extensions/ShellPipelineExtensions.cs index 26dc7b4b252..21a04fa9756 100644 --- a/src/OrchardCore/OrchardCore/Modules/Extensions/ShellPipelineExtensions.cs +++ b/src/OrchardCore/OrchardCore/Modules/Extensions/ShellPipelineExtensions.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using OrchardCore.Environment.Shell; using OrchardCore.Environment.Shell.Builders; @@ -17,6 +18,8 @@ namespace OrchardCore.Modules; public static class ShellPipelineExtensions { + private const string EndpointRouteBuilder = "__EndpointRouteBuilder"; + private static readonly ConcurrentDictionary _semaphores = new(); /// @@ -31,7 +34,7 @@ public static async Task BuildPipelineAsync(this ShellContext context) { if (!context.HasPipeline()) { - context.Pipeline = context.BuildPipeline(); + context.Pipeline = await context.BuildPipelineInternalAsync(); } } finally @@ -43,22 +46,20 @@ public static async Task BuildPipelineAsync(this ShellContext context) /// /// Builds the tenant pipeline. /// - private static IShellPipeline BuildPipeline(this ShellContext context) + private static async ValueTask BuildPipelineInternalAsync(this ShellContext context) { var features = context.ServiceProvider.GetService()?.Features; var builder = new ApplicationBuilder(context.ServiceProvider, features ?? new FeatureCollection()); var startupFilters = builder.ApplicationServices.GetService>(); - Action configure = builder => - { - ConfigurePipeline(builder); - }; - + Action configure = builder => { }; foreach (var filter in startupFilters.Reverse()) { configure = filter.Configure(configure); } + await ConfigurePipelineAsync(builder); + configure(builder); var shellPipeline = new ShellRequestPipeline @@ -72,19 +73,35 @@ private static IShellPipeline BuildPipeline(this ShellContext context) /// /// Configures the tenant pipeline. /// - private static void ConfigurePipeline(IApplicationBuilder builder) + private static async ValueTask ConfigurePipelineAsync(IApplicationBuilder builder) { // 'IStartup' instances are ordered by module dependencies with a 'ConfigureOrder' of 0 by default. // 'OrderBy' performs a stable sort, so the order is preserved among equal 'ConfigureOrder' values. var startups = builder.ApplicationServices.GetServices().OrderBy(s => s.ConfigureOrder); + // Should be done first. + builder.UseRouting(); + + // Try to retrieve the current 'IEndpointRouteBuilder'. + if (!builder.Properties.TryGetValue(EndpointRouteBuilder, out var obj) || + obj is not IEndpointRouteBuilder routes) + { + throw new InvalidOperationException("Failed to retrieve the current endpoint route builder."); + } + + // Routes can be then configured outside 'UseEndpoints()'. var services = ShellScope.Services; - builder.UseRouting().UseEndpoints(routes => + foreach (var startup in startups) { - foreach (var startup in startups) + if (startup is IAsyncStartup asyncStartup) { - startup.Configure(builder, routes, services); + await asyncStartup.ConfigureAsync(builder, routes, services); } - }); + + startup.Configure(builder, routes, services); + } + + // Knowing that routes are already configured. + builder.UseEndpoints(routes => { }); } }