diff --git a/src/OrchardCore.Modules/OrchardCore.AutoSetup/AutoSetupMiddleware.cs b/src/OrchardCore.Modules/OrchardCore.AutoSetup/AutoSetupMiddleware.cs
index 19411e2f406..cdb8a7e4b3d 100644
--- a/src/OrchardCore.Modules/OrchardCore.AutoSetup/AutoSetupMiddleware.cs
+++ b/src/OrchardCore.Modules/OrchardCore.AutoSetup/AutoSetupMiddleware.cs
@@ -1,14 +1,11 @@
-using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
-using OrchardCore.Abstractions.Setup;
using OrchardCore.AutoSetup.Extensions;
using OrchardCore.AutoSetup.Options;
+using OrchardCore.AutoSetup.Services;
using OrchardCore.Environment.Shell;
using OrchardCore.Locking.Distributed;
-using OrchardCore.Setup.Services;
namespace OrchardCore.AutoSetup;
@@ -47,11 +44,6 @@ public class AutoSetupMiddleware
///
private readonly AutoSetupOptions _options;
- ///
- /// The logger.
- ///
- private readonly ILogger _logger;
-
///
/// The auto setup lock options.
///
@@ -70,16 +62,14 @@ public class AutoSetupMiddleware
/// The shell settings.
/// The shell settings manager.
/// The distributed lock.
- /// The auto setup options.
- /// The logger.
+ /// The auto setup options.
public AutoSetupMiddleware(
RequestDelegate next,
IShellHost shellHost,
ShellSettings shellSettings,
IShellSettingsManager shellSettingsManager,
IDistributedLock distributedLock,
- IOptions options,
- ILogger logger)
+ IOptions options)
{
_next = next;
_shellHost = shellHost;
@@ -87,8 +77,6 @@ public AutoSetupMiddleware(
_shellSettingsManager = shellSettingsManager;
_distributedLock = distributedLock;
_options = options.Value;
- _logger = logger;
-
_lockOptions = _options.LockOptions;
_setupOptions = _options.Tenants.FirstOrDefault(options => _shellSettings.Name == options.ShellName);
}
@@ -124,20 +112,23 @@ public async Task InvokeAsync(HttpContext httpContext)
}
// Check if the tenant was installed by another instance.
- using var settings = (await _shellSettingsManager
- .LoadSettingsAsync(_shellSettings.Name))
- .AsDisposable();
+ using var settings = await _shellSettingsManager.LoadSettingsAsync(_shellSettings.Name);
- if (!settings.IsUninitialized())
+ if (settings != null)
{
- await _shellHost.ReloadShellContextAsync(_shellSettings, eventSource: false);
- httpContext.Response.Redirect(pathBase);
-
- return;
+ settings.AsDisposable();
+ if (!settings.IsUninitialized())
+ {
+ await _shellHost.ReloadShellContextAsync(_shellSettings, eventSource: false);
+ httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
+ await httpContext.Response.WriteAsync("The requested tenant is not initialized.");
+ return;
+ }
}
- var setupService = httpContext.RequestServices.GetRequiredService();
- if (await SetupTenantAsync(setupService, _setupOptions, _shellSettings))
+ var autoSetupService = httpContext.RequestServices.GetRequiredService();
+ (var setupContext, var isSuccess) = await autoSetupService.SetupTenantAsync(_setupOptions, _shellSettings);
+ if (isSuccess)
{
if (_setupOptions.IsDefault)
{
@@ -146,13 +137,17 @@ public async Task InvokeAsync(HttpContext httpContext)
{
if (_setupOptions != setupOptions)
{
- await CreateTenantSettingsAsync(setupOptions);
+ await autoSetupService.CreateTenantSettingsAsync(setupOptions);
}
}
}
httpContext.Response.Redirect(pathBase);
-
+ }
+ else
+ {
+ httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
+ await httpContext.Response.WriteAsync($"The AutoSetup failed installing the site.");
return;
}
}
@@ -160,109 +155,4 @@ public async Task InvokeAsync(HttpContext httpContext)
await _next.Invoke(httpContext);
}
-
- ///
- /// Sets up a tenant.
- ///
- /// The setup service.
- /// The tenant setup options.
- /// The tenant shell settings.
- ///
- /// Returns true if successfully setup.
- ///
- public async Task SetupTenantAsync(ISetupService setupService, TenantSetupOptions setupOptions, ShellSettings shellSettings)
- {
- var setupContext = await GetSetupContextAsync(setupOptions, setupService, shellSettings);
-
- _logger.LogInformation("AutoSetup is initializing the site");
-
- await setupService.SetupAsync(setupContext);
-
- if (setupContext.Errors.Count == 0)
- {
- _logger.LogInformation("AutoSetup successfully provisioned the site '{SiteName}'.", setupOptions.SiteName);
-
- return true;
- }
-
- var stringBuilder = new StringBuilder();
- foreach (var error in setupContext.Errors)
- {
- stringBuilder.AppendLine($"{error.Key} : '{error.Value}'");
- }
-
- _logger.LogError("AutoSetup failed installing the site '{SiteName}' with errors: {Errors}", setupOptions.SiteName, stringBuilder);
-
- return false;
- }
-
- ///
- /// Creates a tenant shell settings.
- ///
- /// The setup options.
- /// The .
- public async Task CreateTenantSettingsAsync(TenantSetupOptions setupOptions)
- {
- using var shellSettings = _shellSettingsManager
- .CreateDefaultSettings()
- .AsUninitialized()
- .AsDisposable();
-
- shellSettings.Name = setupOptions.ShellName;
- shellSettings.RequestUrlHost = setupOptions.RequestUrlHost;
- shellSettings.RequestUrlPrefix = setupOptions.RequestUrlPrefix;
-
- shellSettings["ConnectionString"] = setupOptions.DatabaseConnectionString;
- shellSettings["TablePrefix"] = setupOptions.DatabaseTablePrefix;
- shellSettings["Schema"] = setupOptions.DatabaseSchema;
- shellSettings["DatabaseProvider"] = setupOptions.DatabaseProvider;
- shellSettings["Secret"] = Guid.NewGuid().ToString();
- shellSettings["RecipeName"] = setupOptions.RecipeName;
- shellSettings["FeatureProfile"] = setupOptions.FeatureProfile;
-
- await _shellHost.UpdateShellSettingsAsync(shellSettings);
-
- return shellSettings;
- }
-
- ///
- /// Gets a setup context from the configuration.
- ///
- /// The tenant setup options.
- /// The setup service.
- /// The tenant shell settings.
- /// The used to setup the site.
- private static async Task GetSetupContextAsync(TenantSetupOptions options, ISetupService setupService, ShellSettings shellSettings)
- {
- var recipes = await setupService.GetSetupRecipesAsync();
-
- var recipe = recipes.SingleOrDefault(r => r.Name == options.RecipeName);
-
- var setupContext = new SetupContext
- {
- Recipe = recipe,
- ShellSettings = shellSettings,
- Errors = new Dictionary()
- };
-
- if (shellSettings.IsDefaultShell())
- {
- // The 'Default' shell is first created by the infrastructure,
- // so the following 'Autosetup' options need to be passed.
- shellSettings.RequestUrlHost = options.RequestUrlHost;
- shellSettings.RequestUrlPrefix = options.RequestUrlPrefix;
- }
-
- setupContext.Properties[SetupConstants.AdminEmail] = options.AdminEmail;
- setupContext.Properties[SetupConstants.AdminPassword] = options.AdminPassword;
- setupContext.Properties[SetupConstants.AdminUsername] = options.AdminUsername;
- setupContext.Properties[SetupConstants.DatabaseConnectionString] = options.DatabaseConnectionString;
- setupContext.Properties[SetupConstants.DatabaseProvider] = options.DatabaseProvider;
- setupContext.Properties[SetupConstants.DatabaseTablePrefix] = options.DatabaseTablePrefix;
- setupContext.Properties[SetupConstants.DatabaseSchema] = options.DatabaseSchema;
- setupContext.Properties[SetupConstants.SiteName] = options.SiteName;
- setupContext.Properties[SetupConstants.SiteTimeZone] = options.SiteTimeZone;
-
- return setupContext;
- }
}
diff --git a/src/OrchardCore.Modules/OrchardCore.AutoSetup/Services/AutoSetupService.cs b/src/OrchardCore.Modules/OrchardCore.AutoSetup/Services/AutoSetupService.cs
new file mode 100644
index 00000000000..fa16bb59dfc
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.AutoSetup/Services/AutoSetupService.cs
@@ -0,0 +1,111 @@
+using System.Text;
+using Microsoft.Extensions.Logging;
+using OrchardCore.Abstractions.Setup;
+using OrchardCore.AutoSetup.Options;
+using OrchardCore.Environment.Shell;
+using OrchardCore.Setup.Services;
+
+namespace OrchardCore.AutoSetup.Services;
+
+public class AutoSetupService : IAutoSetupService
+{
+ private readonly IShellHost _shellHost;
+ private readonly IShellSettingsManager _shellSettingsManager;
+ private readonly ISetupService _setupService;
+ private readonly ILogger _logger;
+
+ public AutoSetupService(
+ IShellHost shellHost,
+ IShellSettingsManager shellSettingsManager,
+ ISetupService setupService,
+ ILogger logger
+ )
+ {
+ _shellHost = shellHost;
+ _shellSettingsManager = shellSettingsManager;
+ _setupService = setupService;
+ _logger = logger;
+ }
+
+ public async Task<(SetupContext, bool)> SetupTenantAsync(TenantSetupOptions setupOptions, ShellSettings shellSettings)
+ {
+ var setupContext = await GetSetupContextAsync(setupOptions, shellSettings);
+
+ _logger.LogInformation("The AutoSetup is initializing the site.");
+
+ await _setupService.SetupAsync(setupContext);
+
+ if (setupContext.Errors.Count == 0)
+ {
+ _logger.LogInformation("The AutoSetup successfully provisioned the site '{SiteName}'.", setupOptions.SiteName);
+
+ return (setupContext, true);
+ }
+
+ var stringBuilder = new StringBuilder();
+ foreach (var error in setupContext.Errors)
+ {
+ stringBuilder.AppendLine($"{error.Key} : '{error.Value}'");
+ }
+
+ _logger.LogError("The AutoSetup failed installing the site '{SiteName}' with errors: {Errors}.", setupOptions.SiteName, stringBuilder);
+
+ return (setupContext, false);
+ }
+
+ public async Task CreateTenantSettingsAsync(TenantSetupOptions setupOptions)
+ {
+ using var shellSettings = _shellSettingsManager
+ .CreateDefaultSettings()
+ .AsUninitialized()
+ .AsDisposable();
+
+ shellSettings.Name = setupOptions.ShellName;
+ shellSettings.RequestUrlHost = setupOptions.RequestUrlHost;
+ shellSettings.RequestUrlPrefix = setupOptions.RequestUrlPrefix;
+ shellSettings["ConnectionString"] = setupOptions.DatabaseConnectionString;
+ shellSettings["TablePrefix"] = setupOptions.DatabaseTablePrefix;
+ shellSettings["Schema"] = setupOptions.DatabaseSchema;
+ shellSettings["DatabaseProvider"] = setupOptions.DatabaseProvider;
+ shellSettings["Secret"] = Guid.NewGuid().ToString();
+ shellSettings["RecipeName"] = setupOptions.RecipeName;
+ shellSettings["FeatureProfile"] = setupOptions.FeatureProfile;
+
+ await _shellHost.UpdateShellSettingsAsync(shellSettings);
+
+ return shellSettings;
+ }
+
+ public async Task GetSetupContextAsync(TenantSetupOptions options, ShellSettings shellSettings)
+ {
+ var recipe = (await _setupService.GetSetupRecipesAsync())
+ .SingleOrDefault(r => r.Name == options.RecipeName);
+
+ var setupContext = new SetupContext
+ {
+ Recipe = recipe,
+ ShellSettings = shellSettings,
+ Errors = new Dictionary()
+ };
+
+ if (shellSettings.IsDefaultShell())
+ {
+ // The 'Default' shell is first created by the infrastructure,
+ // so the following 'Autosetup' options need to be passed.
+ shellSettings.RequestUrlHost = options.RequestUrlHost;
+ shellSettings.RequestUrlPrefix = options.RequestUrlPrefix;
+ }
+
+ setupContext.Properties[SetupConstants.AdminEmail] = options.AdminEmail;
+ setupContext.Properties[SetupConstants.AdminPassword] = options.AdminPassword;
+ setupContext.Properties[SetupConstants.AdminUsername] = options.AdminUsername;
+ setupContext.Properties[SetupConstants.DatabaseConnectionString] = options.DatabaseConnectionString;
+ setupContext.Properties[SetupConstants.DatabaseProvider] = options.DatabaseProvider;
+ setupContext.Properties[SetupConstants.DatabaseTablePrefix] = options.DatabaseTablePrefix;
+ setupContext.Properties[SetupConstants.DatabaseSchema] = options.DatabaseSchema;
+ setupContext.Properties[SetupConstants.SiteName] = options.SiteName;
+ setupContext.Properties[SetupConstants.SiteTimeZone] = options.SiteTimeZone;
+
+ return setupContext;
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.AutoSetup/Services/IAutoSetupService.cs b/src/OrchardCore.Modules/OrchardCore.AutoSetup/Services/IAutoSetupService.cs
new file mode 100644
index 00000000000..93f46b1288d
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.AutoSetup/Services/IAutoSetupService.cs
@@ -0,0 +1,33 @@
+using OrchardCore.AutoSetup.Options;
+using OrchardCore.Environment.Shell;
+using OrchardCore.Setup.Services;
+
+namespace OrchardCore.AutoSetup.Services;
+
+public interface IAutoSetupService
+{
+ ///
+ /// Creates a tenant shell settings.
+ ///
+ /// The setup options.
+ /// The .
+ Task CreateTenantSettingsAsync(TenantSetupOptions setupOptions);
+
+ ///
+ /// Gets a setup context from the configuration.
+ ///
+ /// The tenant setup options.
+ /// The tenant shell settings.
+ /// The used to setup the site.
+ Task GetSetupContextAsync(TenantSetupOptions options, ShellSettings shellSettings);
+
+ ///
+ /// Sets up a tenant.
+ ///
+ /// The tenant setup options.
+ /// The tenant shell settings.
+ ///
+ /// Returns if successfully setup.
+ ///
+ Task<(SetupContext, bool)> SetupTenantAsync(TenantSetupOptions setupOptions, ShellSettings shellSettings);
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.AutoSetup/Startup.cs b/src/OrchardCore.Modules/OrchardCore.AutoSetup/Startup.cs
index fe2567d190e..8198afb7e3c 100644
--- a/src/OrchardCore.Modules/OrchardCore.AutoSetup/Startup.cs
+++ b/src/OrchardCore.Modules/OrchardCore.AutoSetup/Startup.cs
@@ -7,6 +7,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OrchardCore.AutoSetup.Options;
+using OrchardCore.AutoSetup.Services;
using OrchardCore.Environment.Shell;
using OrchardCore.Environment.Shell.Configuration;
using OrchardCore.Modules;
@@ -67,6 +68,8 @@ public override void ConfigureServices(IServiceCollection services)
{
services.Configure(o => o.ConfigurationExists = true);
}
+
+ services.AddScoped();
}
///
diff --git a/test/OrchardCore.Tests/Modules/OrchardCore.AutoSetup/AutoSetupMiddlewareTests.cs b/test/OrchardCore.Tests/Modules/OrchardCore.AutoSetup/AutoSetupMiddlewareTests.cs
new file mode 100644
index 00000000000..522080760cd
--- /dev/null
+++ b/test/OrchardCore.Tests/Modules/OrchardCore.AutoSetup/AutoSetupMiddlewareTests.cs
@@ -0,0 +1,148 @@
+using OrchardCore.AutoSetup;
+using OrchardCore.AutoSetup.Options;
+using OrchardCore.AutoSetup.Services;
+using OrchardCore.Environment.Shell;
+using OrchardCore.Environment.Shell.Models;
+using OrchardCore.Locking;
+using OrchardCore.Locking.Distributed;
+using OrchardCore.Setup.Services;
+
+namespace OrchardCore.Tests.Modules.OrchardCore.AutoSetup;
+
+public class AutoSetupMiddlewareTests
+{
+ private readonly Mock _mockShellHost;
+ private readonly ShellSettings _shellSettings;
+ private readonly Mock _mockShellSettingsManager;
+ private readonly Mock _mockDistributedLock;
+ private readonly Mock> _mockOptions;
+ private readonly Mock _mockAutoSetupService;
+ private bool _nextCalled;
+ public AutoSetupMiddlewareTests()
+ {
+ _shellSettings = new ShellSettings();
+ _shellSettings.AsDefaultShell();
+ _mockShellHost = new Mock();
+ _mockShellSettingsManager = new Mock();
+ _mockDistributedLock = new Mock();
+ _mockOptions = new Mock>();
+ _mockAutoSetupService = new Mock();
+
+ _mockOptions.Setup(o => o.Value).Returns(new AutoSetupOptions
+ {
+ LockOptions = new LockOptions(),
+ Tenants = new List
+ {
+ new TenantSetupOptions { ShellName = ShellSettings.DefaultShellName }
+ }
+ });
+ }
+
+ [Fact]
+ public async Task InvokeAsync_InitializedShell_SkipsSetup()
+ {
+ // Arrange
+ _shellSettings.State = TenantState.Running;
+
+ var httpContext = new DefaultHttpContext();
+
+ var middleware = CreateMiddleware();
+
+ // Act
+ await middleware.InvokeAsync(httpContext);
+
+ // Assert
+ Assert.True(_nextCalled);
+ _mockAutoSetupService.Verify(s => s.SetupTenantAsync(It.IsAny(), It.IsAny()), Times.Never);
+ }
+
+ [Fact]
+ public async Task InvokeAsync_FailedSetup_ReturnsServiceUnavailable()
+ {
+ // Arrange
+ _shellSettings.State = TenantState.Uninitialized;
+
+ SetupDistributedLockMock(true);
+
+ var setupContext = new SetupContext { Errors = new Dictionary { { "Error", "Test error" } } };
+ _mockAutoSetupService.Setup(s => s.SetupTenantAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync((setupContext, false));
+
+ var httpContext = new DefaultHttpContext();
+ httpContext.RequestServices = new ServiceCollection()
+ .AddSingleton(_mockAutoSetupService.Object)
+ .BuildServiceProvider();
+
+ var middleware = CreateMiddleware();
+
+ // Act
+ await middleware.InvokeAsync(httpContext);
+
+ // Assert
+ Assert.Equal(StatusCodes.Status503ServiceUnavailable, httpContext.Response.StatusCode);
+ }
+
+ [Fact]
+ public async Task InvokeAsync_UnInitializedShell_PerformsSetup()
+ {
+ // Arrange
+ _shellSettings.State = TenantState.Uninitialized;
+
+ SetupDistributedLockMock(true);
+
+ var setupContext = new SetupContext();
+ _mockAutoSetupService.Setup(s => s.SetupTenantAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync((setupContext, true));
+
+ var httpContext = new DefaultHttpContext();
+ httpContext.RequestServices = new ServiceCollection()
+ .AddSingleton(_mockAutoSetupService.Object)
+ .BuildServiceProvider();
+
+ var middleware = CreateMiddleware();
+ // Act
+ await middleware.InvokeAsync(httpContext);
+
+ // Assert
+ Assert.Equal(StatusCodes.Status302Found, httpContext.Response.StatusCode); // Redirect
+ _mockAutoSetupService.Verify(s => s.SetupTenantAsync(It.IsAny(), It.IsAny()), Times.Once);
+ }
+
+ [Fact]
+ public async Task InvokeAsync_FailedLockAcquisition_ThrowsTimeoutException()
+ {
+ // Arrange
+ _shellSettings.State = TenantState.Uninitialized;
+
+ SetupDistributedLockMock(false);
+
+ var httpContext = new DefaultHttpContext();
+
+ var middleware = CreateMiddleware();
+
+ // Act & Assert
+ await Assert.ThrowsAsync(() => middleware.InvokeAsync(httpContext));
+ }
+
+ private void SetupDistributedLockMock(bool acquireLock)
+ {
+ var mockLocker = new Mock();
+ _mockDistributedLock
+ .Setup(d => d.TryAcquireLockAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync((mockLocker.Object, acquireLock));
+ }
+
+ private AutoSetupMiddleware CreateMiddleware()
+ => new AutoSetupMiddleware(
+ next: (innerHttpContext) =>
+ {
+ _nextCalled = true;
+
+ return Task.CompletedTask;
+ },
+ _mockShellHost.Object,
+ _shellSettings,
+ _mockShellSettingsManager.Object,
+ _mockDistributedLock.Object,
+ _mockOptions.Object);
+}