Skip to content

Commit

Permalink
Startup Async Configure (OrchardCMS#14801)
Browse files Browse the repository at this point in the history
  • Loading branch information
jtkech authored and urbanit committed Mar 18, 2024
1 parent 7926360 commit c65f184
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 18 deletions.
7 changes: 4 additions & 3 deletions src/OrchardCore.Modules/OrchardCore.Localization/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -41,12 +42,12 @@ public override void ConfigureServices(IServiceCollection services)
}

/// <inheritdocs />
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<ILocalizationService>();

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<IOptions<RequestLocalizationOptions>>().Value;
var ignoreSystemSettings = serviceProvider.GetService<IOptions<CultureOptions>>().Value.IgnoreSystemSettings;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -97,6 +98,46 @@ public OrchardCoreBuilder Configure(Action<IApplicationBuilder> configure, int o
return Configure((app, routes, sp) => configure(app), order);
}

/// <summary>
/// This async action gets called for each tenant. Use this method to configure the tenant pipeline.
/// </summary>
/// <param name="configureAsync">The async action to execute when configuring the request's pipeline for a tenant.</param>
/// <param name="order">The order of the action to execute. Lower values will be executed first.</param>
public OrchardCoreBuilder Configure(Func<IApplicationBuilder, IEndpointRouteBuilder, IServiceProvider, ValueTask> configureAsync, int order = 0)
{
if (!_actions.TryGetValue(order, out var actions))
{
actions = _actions[order] = new StartupActions(order);

ApplicationServices.AddTransient<IStartup>(sp => new StartupActionsStartup(
sp.GetRequiredService<IServiceProvider>(), actions, order));
}

actions.AsyncConfigureActions.Add(configureAsync);

return this;
}

/// <summary>
/// This async action gets called for each tenant. Use this method to configure the tenant pipeline.
/// </summary>
/// <param name="configureAsync">The async action to execute when configuring the request's pipeline for a tenant.</param>
/// <param name="order">The order of the action to execute. Lower values will be executed first.</param>
public OrchardCoreBuilder Configure(Func<IApplicationBuilder, IEndpointRouteBuilder, ValueTask> configureAsync, int order = 0)
{
return Configure((app, routes, sp) => configureAsync(app, routes), order);
}

/// <summary>
/// This async action gets called for each tenant. Use this method to configure the tenant pipeline.
/// </summary>
/// <param name="configureAsync">The async action to execute when configuring the request's pipeline for a tenant.</param>
/// <param name="order">The order of the action to execute. Lower values will be executed first.</param>
public OrchardCoreBuilder Configure(Func<IApplicationBuilder, ValueTask> configureAsync, int order = 0)
{
return Configure((app, routes, sp) => configureAsync(app), order);
}

public OrchardCoreBuilder EnableFeature(string id)
{
return ConfigureServices(services =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;

Expand All @@ -14,8 +15,13 @@ public StartupActions(int order)

public int Order { get; }

public ICollection<Action<IServiceCollection, IServiceProvider>> ConfigureServicesActions { get; } = new List<Action<IServiceCollection, IServiceProvider>>();
public ICollection<Action<IServiceCollection, IServiceProvider>> ConfigureServicesActions { get; } =
new List<Action<IServiceCollection, IServiceProvider>>();

public ICollection<Action<IApplicationBuilder, IEndpointRouteBuilder, IServiceProvider>> ConfigureActions { get; } = new List<Action<IApplicationBuilder, IEndpointRouteBuilder, IServiceProvider>>();
public ICollection<Action<IApplicationBuilder, IEndpointRouteBuilder, IServiceProvider>> ConfigureActions { get; } =
new List<Action<IApplicationBuilder, IEndpointRouteBuilder, IServiceProvider>>();

public ICollection<Func<IApplicationBuilder, IEndpointRouteBuilder, IServiceProvider, ValueTask>> AsyncConfigureActions { get; } =
new List<Func<IApplicationBuilder, IEndpointRouteBuilder, IServiceProvider, ValueTask>>();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -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);
}
}
}
}
21 changes: 21 additions & 0 deletions src/OrchardCore/OrchardCore.Abstractions/Modules/IAsyncStartup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;

namespace OrchardCore.Modules
{
/// <summary>
/// An implementation of this interface allows to configure asynchronously the tenant pipeline.
/// </summary>
public interface IAsyncStartup
{
/// <summary>
/// This method gets called by the runtime. Use this method to configure the tenant pipeline.
/// </summary>
/// <param name="builder"></param>
/// <param name="routes"></param>
/// <param name="serviceProvider"></param>
ValueTask ConfigureAsync(IApplicationBuilder builder, IEndpointRouteBuilder routes, IServiceProvider serviceProvider);
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <inheritdoc />
public virtual int Order { get; } = 0;
Expand All @@ -22,5 +23,8 @@ public virtual void ConfigureServices(IServiceCollection services)
public virtual void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider)
{
}

/// <inheritdoc />
public virtual ValueTask ConfigureAsync(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) => default;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -17,6 +18,8 @@ namespace OrchardCore.Modules;

public static class ShellPipelineExtensions
{
private const string EndpointRouteBuilder = "__EndpointRouteBuilder";

private static readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphores = new();

/// <summary>
Expand All @@ -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
Expand All @@ -43,22 +46,20 @@ public static async Task BuildPipelineAsync(this ShellContext context)
/// <summary>
/// Builds the tenant pipeline.
/// </summary>
private static IShellPipeline BuildPipeline(this ShellContext context)
private static async ValueTask<IShellPipeline> BuildPipelineInternalAsync(this ShellContext context)
{
var features = context.ServiceProvider.GetService<IServer>()?.Features;
var builder = new ApplicationBuilder(context.ServiceProvider, features ?? new FeatureCollection());
var startupFilters = builder.ApplicationServices.GetService<IEnumerable<IStartupFilter>>();

Action<IApplicationBuilder> configure = builder =>
{
ConfigurePipeline(builder);
};

Action<IApplicationBuilder> configure = builder => { };
foreach (var filter in startupFilters.Reverse())
{
configure = filter.Configure(configure);
}

await ConfigurePipelineAsync(builder);

configure(builder);

var shellPipeline = new ShellRequestPipeline
Expand All @@ -72,19 +73,35 @@ private static IShellPipeline BuildPipeline(this ShellContext context)
/// <summary>
/// Configures the tenant pipeline.
/// </summary>
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<IStartup>().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 => { });
}
}

0 comments on commit c65f184

Please sign in to comment.